mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat/BE/US-278/TASK-288,289-adjust schema database,Create trigger in expense module, add filter in warehouse linked to project flock
This commit is contained in:
@@ -6,12 +6,20 @@ BEGIN
|
||||
ALTER TABLE purchase_items
|
||||
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||
) THEN
|
||||
ALTER TABLE purchase_items
|
||||
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
||||
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE purchase_items
|
||||
DROP COLUMN IF EXISTS expense_nonstock_id,
|
||||
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
||||
ALTER COLUMN vehicle_number DROP NOT NULL,
|
||||
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||
|
||||
@@ -27,4 +35,4 @@ ALTER TABLE purchases
|
||||
|
||||
ALTER TABLE purchases
|
||||
ALTER COLUMN credit_term DROP DEFAULT,
|
||||
ALTER COLUMN grand_total DROP DEFAULT;
|
||||
ALTER COLUMN grand_total DROP DEFAULT;
|
||||
|
||||
@@ -11,7 +11,8 @@ ALTER TABLE purchases
|
||||
|
||||
-- Bring purchase_items in line with new requirements
|
||||
ALTER TABLE purchase_items
|
||||
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT;
|
||||
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||
|
||||
UPDATE purchase_items
|
||||
SET vehicle_number = ''
|
||||
@@ -35,7 +36,22 @@ BEGIN
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
||||
ON purchase_items (expense_nonstock_id);
|
||||
ON purchase_items (expense_nonstock_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
||||
ON purchase_items (project_flock_kandang_id);
|
||||
|
||||
@@ -5,21 +5,22 @@ import (
|
||||
)
|
||||
|
||||
type PurchaseItem struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
PurchaseId uint `gorm:"not null"`
|
||||
ProductId uint `gorm:"not null"`
|
||||
WarehouseId uint `gorm:"not null"`
|
||||
ProductWarehouseId *uint
|
||||
ReceivedDate *time.Time
|
||||
TravelNumber *string
|
||||
TravelNumberDocs *string
|
||||
VehicleNumber *string
|
||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
ExpenseNonstockId *uint64
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
PurchaseId uint `gorm:"not null"`
|
||||
ProductId uint `gorm:"not null"`
|
||||
WarehouseId uint `gorm:"not null"`
|
||||
ProductWarehouseId *uint
|
||||
ProjectFlockKandangId *uint
|
||||
ReceivedDate *time.Time
|
||||
TravelNumber *string
|
||||
TravelNumberDocs *string
|
||||
VehicleNumber *string
|
||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
ExpenseNonstockId *uint64
|
||||
|
||||
// Relations
|
||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||
|
||||
@@ -24,10 +24,11 @@ func NewWarehouseController(warehouseService service.WarehouseService) *Warehous
|
||||
|
||||
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
AreaId: c.QueryInt("area_id", 0),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
AreaId: c.QueryInt("area_id", 0),
|
||||
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -53,11 +53,28 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
db = db.Where("warehouses.name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.AreaId != 0 {
|
||||
db = db.Where("area_id = ?", params.AreaId)
|
||||
}
|
||||
if params.ActiveProjectFlockOnly {
|
||||
db = db.Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM kandangs k
|
||||
JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id
|
||||
JOIN (
|
||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||
ORDER BY approvable_id, action_at DESC
|
||||
) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
|
||||
WHERE k.id = warehouses.kandang_id
|
||||
AND LOWER(latest_approval.step_name) = LOWER(?)
|
||||
)
|
||||
`, "Aktif")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ type Update struct {
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||
ActiveProjectFlockOnly bool `query:"active_project_flock"`
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
warehouseRepo,
|
||||
supplierRepo,
|
||||
productWarehouseRepo,
|
||||
projectFlockKandangRepository,
|
||||
approvalService,
|
||||
expenseBridge,
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ type PurchaseRepository interface {
|
||||
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
||||
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
|
||||
}
|
||||
|
||||
type PurchaseRepositoryImpl struct {
|
||||
@@ -58,6 +59,34 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error {
|
||||
if purchaseID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := `
|
||||
WITH latest_pfk AS (
|
||||
SELECT pfk.id, pfk.kandang_id
|
||||
FROM project_flock_kandangs pfk
|
||||
JOIN (
|
||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||
ORDER BY approvable_id, action_at DESC
|
||||
) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
|
||||
WHERE LOWER(latest_approval.step_name) = LOWER('Aktif')
|
||||
)
|
||||
UPDATE purchase_items pi
|
||||
SET project_flock_kandang_id = lp.id
|
||||
FROM warehouses w
|
||||
JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id
|
||||
WHERE pi.purchase_id = ?
|
||||
AND pi.project_flock_kandang_id IS NULL
|
||||
AND pi.warehouse_id = w.id;
|
||||
`
|
||||
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -303,7 +303,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
}
|
||||
|
||||
itemLinks := make(map[uint]itemLink)
|
||||
existingExpenseByKey := make(map[string]uint64)
|
||||
updatedExpenses := make(map[uint64]struct{})
|
||||
if len(updates) > 0 {
|
||||
ids := make([]uint, 0, len(updates))
|
||||
@@ -341,16 +340,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
Qty: row.Qty,
|
||||
Price: row.Price,
|
||||
}
|
||||
if row.ExpenseID != 0 && row.SupplierID != 0 && !row.TransactionDate.IsZero() {
|
||||
// Use warehouse from purchase item; if not found, skip key.
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == row.ItemID {
|
||||
key := groupingKey(row.SupplierID, row.TransactionDate.UTC().Truncate(24*time.Hour), purchase.Items[i].WarehouseId)
|
||||
existingExpenseByKey[key] = row.ExpenseID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,9 +350,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
}
|
||||
|
||||
groups := make(map[string][]groupedItem)
|
||||
toRecreate := make([]ExpenseReceivingPayload, 0)
|
||||
|
||||
movedFrom := make([]uint64, 0)
|
||||
|
||||
for _, payload := range updates {
|
||||
if payload.ReceivedDate == nil {
|
||||
@@ -383,10 +369,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
supplierID = purchase.SupplierId
|
||||
}
|
||||
|
||||
// Decide whether to update existing expense_nonstock or recreate.
|
||||
// Decide whether to update existing expense_nonstock or create new.
|
||||
link, hasLink := itemLinks[payload.PurchaseItemID]
|
||||
requiresDelete := false
|
||||
handledUpdate := false
|
||||
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
||||
newDate := receivedDate
|
||||
@@ -396,39 +380,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
|
||||
// Supplier/date change: prefer re-link to existing expense in target group; otherwise recreate.
|
||||
if oldSupplier != supplierID || !oldDate.Equal(newDate) {
|
||||
newKey := groupingKey(supplierID, newDate, payload.WarehouseID)
|
||||
if targetExpenseID, ok := existingExpenseByKey[newKey]; ok && targetExpenseID != 0 {
|
||||
// Move nonstock to existing expense header in the target group.
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(map[string]interface{}{
|
||||
"expense_id": targetExpenseID,
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Track cleanup for old header if it becomes empty.
|
||||
movedFrom = append(movedFrom, link.ExpenseID)
|
||||
existingExpenseByKey[newKey] = targetExpenseID
|
||||
updatedExpenses[targetExpenseID] = struct{}{}
|
||||
handledUpdate = true
|
||||
} else {
|
||||
requiresDelete = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here and no delete is required, update the existing nonstock fields and skip creation.
|
||||
if !requiresDelete {
|
||||
// If supplier/date unchanged, update nonstock in place.
|
||||
if oldSupplier == supplierID && oldDate.Equal(newDate) {
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
@@ -443,19 +396,139 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
if link.ExpenseID != 0 {
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
}
|
||||
handledUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Supplier/date changed: if the linked expense has only this nonstock, update it in place.
|
||||
if link.ExpenseID != 0 {
|
||||
var cnt int64
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", link.ExpenseID).
|
||||
Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt == 1 {
|
||||
if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
||||
}
|
||||
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.Expense{}).
|
||||
Where("id = ?", link.ExpenseID).
|
||||
Updates(map[string]interface{}{
|
||||
"transaction_date": newDate,
|
||||
"supplier_id": supplierID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updateBody := map[string]interface{}{
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
"nonstock_id": newNonstockID,
|
||||
"kandang_id": uint64(*item.Warehouse.KandangId),
|
||||
}
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(updateBody).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
// Expense has multiple nonstocks: create new expense header for this item, then move existing nonstock to it.
|
||||
var kandangID *uint
|
||||
var projectFK *uint
|
||||
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||
id := uint(*item.Warehouse.KandangId)
|
||||
kandangID = &id
|
||||
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||
pid := uint(project.Id)
|
||||
projectFK = &pid
|
||||
}
|
||||
}
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
totalPrice := pricePerItem * payload.ReceivedQty
|
||||
|
||||
gItem := groupedItem{
|
||||
item: item,
|
||||
payload: payload,
|
||||
projectFK: projectFK,
|
||||
kandangID: kandangID,
|
||||
totalPrice: totalPrice,
|
||||
}
|
||||
|
||||
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expenseDetail, err := b.createExpenseViaService(c, purchase, []groupedItem{gItem}, newDate, newNonstockID, purchase.PoNumber, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var createdNonstockID uint64
|
||||
if expenseDetail != nil {
|
||||
noteMap := mapExpenseNotes(expenseDetail)
|
||||
createdNonstockID = noteMap[payload.PurchaseItemID]
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
updateBody := map[string]interface{}{
|
||||
"expense_id": expenseDetail.Id,
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
"nonstock_id": newNonstockID,
|
||||
}
|
||||
if kandangID != nil {
|
||||
updateBody["kandang_id"] = uint64(*kandangID)
|
||||
}
|
||||
if projectFK != nil {
|
||||
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
|
||||
}
|
||||
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(updateBody).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createdNonstockID != 0 {
|
||||
if err := b.db.WithContext(ctx).Delete(&entity.ExpenseNonstock{}, createdNonstockID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if link.ExpenseID != 0 {
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
}
|
||||
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise create new expense/nonstock in grouping flow.
|
||||
}
|
||||
|
||||
if requiresDelete {
|
||||
toRecreate = append(toRecreate, payload)
|
||||
continue
|
||||
baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||
key := baseKey
|
||||
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||
key = fmt.Sprintf("%s:%d", baseKey, payload.PurchaseItemID)
|
||||
}
|
||||
if handledUpdate {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||
|
||||
var kandangID *uint
|
||||
var projectFK *uint
|
||||
@@ -481,54 +554,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
kandangID: kandangID,
|
||||
totalPrice: totalPrice,
|
||||
})
|
||||
if existingID, ok := existingExpenseByKey[key]; ok && existingID != 0 {
|
||||
updatedExpenses[existingID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// For payloads that require delete/recreate, clean up their old links first.
|
||||
if len(toRecreate) > 0 {
|
||||
if err := b.cleanupExistingNonstocks(ctx, toRecreate); err != nil {
|
||||
return err
|
||||
}
|
||||
// Then add them back into grouping for creation.
|
||||
for _, payload := range toRecreate {
|
||||
item := itemMap[payload.PurchaseItemID]
|
||||
if item == nil || payload.ReceivedDate == nil {
|
||||
continue
|
||||
}
|
||||
receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
|
||||
supplierID := payload.SupplierID
|
||||
if supplierID == 0 {
|
||||
supplierID = purchase.SupplierId
|
||||
}
|
||||
key := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||
|
||||
var kandangID *uint
|
||||
var projectFK *uint
|
||||
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||
id := uint(*item.Warehouse.KandangId)
|
||||
kandangID = &id
|
||||
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||
pid := uint(project.Id)
|
||||
projectFK = &pid
|
||||
}
|
||||
}
|
||||
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
totalPrice := pricePerItem * payload.ReceivedQty
|
||||
|
||||
groups[key] = append(groups[key], groupedItem{
|
||||
item: item,
|
||||
payload: payload,
|
||||
projectFK: projectFK,
|
||||
kandangID: kandangID,
|
||||
totalPrice: totalPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for key, items := range groups {
|
||||
@@ -536,7 +561,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) != 3 {
|
||||
if len(parts) < 3 {
|
||||
return errors.New("invalid expense grouping key")
|
||||
}
|
||||
expenseDate, err := utils.ParseDateString(parts[1])
|
||||
@@ -566,13 +591,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup old expense headers that became empty after re-link.
|
||||
if len(movedFrom) > 0 {
|
||||
if err := b.cleanupEmptyExpenses(ctx, movedFrom); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(updatedExpenses) > 0 {
|
||||
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
|
||||
return err
|
||||
@@ -691,25 +709,7 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
|
||||
return nil
|
||||
}
|
||||
|
||||
noteToExpenseNonstock := make(map[uint]uint64)
|
||||
for _, kandang := range detail.Kandangs {
|
||||
for _, pengajuan := range kandang.Pengajuans {
|
||||
note := strings.TrimSpace(pengajuan.Notes)
|
||||
if note == "" {
|
||||
continue
|
||||
}
|
||||
const prefix = "purchase_item:"
|
||||
if !strings.HasPrefix(note, prefix) {
|
||||
continue
|
||||
}
|
||||
idStr := strings.TrimPrefix(note, prefix)
|
||||
var itemID uint
|
||||
if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
|
||||
continue
|
||||
}
|
||||
noteToExpenseNonstock[itemID] = pengajuan.Id
|
||||
}
|
||||
}
|
||||
noteToExpenseNonstock := mapExpenseNotes(detail)
|
||||
|
||||
if len(noteToExpenseNonstock) == 0 {
|
||||
return nil
|
||||
@@ -730,3 +730,29 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
|
||||
result := make(map[uint]uint64)
|
||||
if detail == nil {
|
||||
return result
|
||||
}
|
||||
for _, kandang := range detail.Kandangs {
|
||||
for _, pengajuan := range kandang.Pengajuans {
|
||||
note := strings.TrimSpace(pengajuan.Notes)
|
||||
if note == "" {
|
||||
continue
|
||||
}
|
||||
const prefix = "purchase_item:"
|
||||
if !strings.HasPrefix(note, prefix) {
|
||||
continue
|
||||
}
|
||||
idStr := strings.TrimPrefix(note, prefix)
|
||||
var itemID uint
|
||||
if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
|
||||
continue
|
||||
}
|
||||
result[itemID] = pengajuan.Id
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -43,16 +44,17 @@ const (
|
||||
)
|
||||
|
||||
type purchaseService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
PurchaseRepo rPurchase.PurchaseRepository
|
||||
ProductRepo rProduct.ProductRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseBridge PurchaseExpenseBridge
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
PurchaseRepo rPurchase.PurchaseRepository
|
||||
ProductRepo rProduct.ProductRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseBridge PurchaseExpenseBridge
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
|
||||
type staffAdjustmentPayload struct {
|
||||
@@ -67,20 +69,22 @@ func NewPurchaseService(
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
supplierRepo rSupplier.SupplierRepository,
|
||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
expenseBridge PurchaseExpenseBridge,
|
||||
) PurchaseService {
|
||||
return &purchaseService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
ProductRepo: productRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseBridge: expenseBridge,
|
||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
ProductRepo: productRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseBridge: expenseBridge,
|
||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||
}
|
||||
}
|
||||
func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
@@ -221,6 +225,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
productId uint
|
||||
warehouseId uint
|
||||
subQty float64
|
||||
pfkID *uint
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
@@ -229,9 +234,9 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
|
||||
warehouseCache := make(map[uint]*entity.Warehouse)
|
||||
productSupplierCache := make(map[uint]bool)
|
||||
getWarehouse := func(id uint) (*entity.Warehouse, error) {
|
||||
getWarehouse := func(id uint) (*entity.Warehouse, *uint, error) {
|
||||
if warehouse, ok := warehouseCache[id]; ok {
|
||||
return warehouse, nil
|
||||
return warehouse, nil, nil
|
||||
}
|
||||
warehouse, err := s.WarehouseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Area").Preload("Location")
|
||||
@@ -239,21 +244,37 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
||||
return nil, nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
}
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
||||
}
|
||||
var pfkID *uint
|
||||
if s.ProjectFlockKandangRepo != nil {
|
||||
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
||||
idCopy := uint(pfk.Id)
|
||||
pfkID = &idCopy
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d has no active project flock", id))
|
||||
} else if err != nil {
|
||||
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||
}
|
||||
}
|
||||
|
||||
warehouseCache[id] = warehouse
|
||||
return warehouse, nil
|
||||
return warehouse, pfkID, nil
|
||||
}
|
||||
|
||||
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||
indexMap := make(map[string]int)
|
||||
|
||||
for _, item := range req.Items {
|
||||
if _, err := getWarehouse(item.WarehouseID); err != nil {
|
||||
_, pfkID, err := getWarehouse(item.WarehouseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -282,6 +303,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
productId: productId,
|
||||
warehouseId: warehouseId,
|
||||
subQty: item.Quantity,
|
||||
pfkID: pfkID,
|
||||
}
|
||||
aggregated = append(aggregated, entry)
|
||||
indexMap[key] = len(aggregated) - 1
|
||||
@@ -308,14 +330,15 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
emptyVehicle := ""
|
||||
for _, item := range aggregated {
|
||||
items = append(items, &entity.PurchaseItem{
|
||||
ProductId: item.productId,
|
||||
WarehouseId: item.warehouseId,
|
||||
SubQty: item.subQty,
|
||||
TotalQty: 0,
|
||||
TotalUsed: 0,
|
||||
Price: 0,
|
||||
TotalPrice: 0,
|
||||
VehicleNumber: &emptyVehicle,
|
||||
ProductId: item.productId,
|
||||
WarehouseId: item.warehouseId,
|
||||
ProjectFlockKandangId: item.pfkID,
|
||||
SubQty: item.subQty,
|
||||
TotalQty: 0,
|
||||
TotalUsed: 0,
|
||||
Price: 0,
|
||||
TotalPrice: 0,
|
||||
VehicleNumber: &emptyVehicle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -332,6 +355,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
return err
|
||||
}
|
||||
|
||||
if err := purchaseRepoTx.BackfillProjectFlockKandang(c.Context(), purchase.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID := uint(purchase.CreatedBy)
|
||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepPengajuan, entity.ApprovalActionCreated, actorID, nil, false); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user