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
|
ALTER TABLE purchase_items
|
||||||
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||||
END IF;
|
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 $$;
|
END $$;
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
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
|
ALTER TABLE purchase_items
|
||||||
DROP COLUMN IF EXISTS expense_nonstock_id,
|
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 DROP NOT NULL,
|
||||||
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ ALTER TABLE purchases
|
|||||||
|
|
||||||
-- Bring purchase_items in line with new requirements
|
-- Bring purchase_items in line with new requirements
|
||||||
ALTER TABLE purchase_items
|
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
|
UPDATE purchase_items
|
||||||
SET vehicle_number = ''
|
SET vehicle_number = ''
|
||||||
@@ -35,7 +36,22 @@ BEGIN
|
|||||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
END IF;
|
END IF;
|
||||||
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 $$;
|
END $$;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
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 {
|
type PurchaseItem struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PurchaseId uint `gorm:"not null"`
|
PurchaseId uint `gorm:"not null"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint
|
ProductWarehouseId *uint
|
||||||
ReceivedDate *time.Time
|
ProjectFlockKandangId *uint
|
||||||
TravelNumber *string
|
ReceivedDate *time.Time
|
||||||
TravelNumberDocs *string
|
TravelNumber *string
|
||||||
VehicleNumber *string
|
TravelNumberDocs *string
|
||||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
VehicleNumber *string
|
||||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
ExpenseNonstockId *uint64
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
ExpenseNonstockId *uint64
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
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 {
|
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
AreaId: c.QueryInt("area_id", 0),
|
AreaId: c.QueryInt("area_id", 0),
|
||||||
|
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
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 {
|
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
db = db.Where("warehouses.name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
if params.AreaId != 0 {
|
if params.AreaId != 0 {
|
||||||
db = db.Where("area_id = ?", params.AreaId)
|
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")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
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,
|
warehouseRepo,
|
||||||
supplierRepo,
|
supplierRepo,
|
||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
|
projectFlockKandangRepository,
|
||||||
approvalService,
|
approvalService,
|
||||||
expenseBridge,
|
expenseBridge,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type PurchaseRepository interface {
|
|||||||
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
||||||
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
NextPoNumber(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 {
|
type PurchaseRepositoryImpl struct {
|
||||||
@@ -58,6 +59,34 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
|
|||||||
return nil
|
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 {
|
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemLinks := make(map[uint]itemLink)
|
itemLinks := make(map[uint]itemLink)
|
||||||
existingExpenseByKey := make(map[string]uint64)
|
|
||||||
updatedExpenses := make(map[uint64]struct{})
|
updatedExpenses := make(map[uint64]struct{})
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
ids := make([]uint, 0, len(updates))
|
ids := make([]uint, 0, len(updates))
|
||||||
@@ -341,16 +340,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
Qty: row.Qty,
|
Qty: row.Qty,
|
||||||
Price: row.Price,
|
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)
|
groups := make(map[string][]groupedItem)
|
||||||
toRecreate := make([]ExpenseReceivingPayload, 0)
|
|
||||||
|
|
||||||
movedFrom := make([]uint64, 0)
|
|
||||||
|
|
||||||
for _, payload := range updates {
|
for _, payload := range updates {
|
||||||
if payload.ReceivedDate == nil {
|
if payload.ReceivedDate == nil {
|
||||||
@@ -383,10 +369,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
supplierID = purchase.SupplierId
|
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]
|
link, hasLink := itemLinks[payload.PurchaseItemID]
|
||||||
requiresDelete := false
|
|
||||||
handledUpdate := false
|
|
||||||
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||||
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
||||||
newDate := receivedDate
|
newDate := receivedDate
|
||||||
@@ -396,39 +380,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
pricePerItem = *payload.TransportPerItem
|
pricePerItem = *payload.TransportPerItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supplier/date change: prefer re-link to existing expense in target group; otherwise recreate.
|
// If supplier/date unchanged, update nonstock in place.
|
||||||
if oldSupplier != supplierID || !oldDate.Equal(newDate) {
|
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 {
|
|
||||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
if err := b.db.WithContext(ctx).
|
if err := b.db.WithContext(ctx).
|
||||||
Model(&entity.ExpenseNonstock{}).
|
Model(&entity.ExpenseNonstock{}).
|
||||||
@@ -443,19 +396,139 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
if link.ExpenseID != 0 {
|
if link.ExpenseID != 0 {
|
||||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
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 {
|
baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||||
toRecreate = append(toRecreate, payload)
|
key := baseKey
|
||||||
continue
|
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 kandangID *uint
|
||||||
var projectFK *uint
|
var projectFK *uint
|
||||||
@@ -481,54 +554,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
kandangID: kandangID,
|
kandangID: kandangID,
|
||||||
totalPrice: totalPrice,
|
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 {
|
for key, items := range groups {
|
||||||
@@ -536,7 +561,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts := strings.Split(key, ":")
|
parts := strings.Split(key, ":")
|
||||||
if len(parts) != 3 {
|
if len(parts) < 3 {
|
||||||
return errors.New("invalid expense grouping key")
|
return errors.New("invalid expense grouping key")
|
||||||
}
|
}
|
||||||
expenseDate, err := utils.ParseDateString(parts[1])
|
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 len(updatedExpenses) > 0 {
|
||||||
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
|
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -691,25 +709,7 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
noteToExpenseNonstock := make(map[uint]uint64)
|
noteToExpenseNonstock := mapExpenseNotes(detail)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(noteToExpenseNonstock) == 0 {
|
if len(noteToExpenseNonstock) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -730,3 +730,29 @@ func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail
|
|||||||
|
|
||||||
return nil
|
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"
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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"
|
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"
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -43,16 +44,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type purchaseService struct {
|
type purchaseService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
PurchaseRepo rPurchase.PurchaseRepository
|
PurchaseRepo rPurchase.PurchaseRepository
|
||||||
ProductRepo rProduct.ProductRepository
|
ProductRepo rProduct.ProductRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
SupplierRepo rSupplier.SupplierRepository
|
SupplierRepo rSupplier.SupplierRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
ExpenseBridge PurchaseExpenseBridge
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
ExpenseBridge PurchaseExpenseBridge
|
||||||
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type staffAdjustmentPayload struct {
|
type staffAdjustmentPayload struct {
|
||||||
@@ -67,20 +69,22 @@ func NewPurchaseService(
|
|||||||
warehouseRepo rWarehouse.WarehouseRepository,
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
supplierRepo rSupplier.SupplierRepository,
|
supplierRepo rSupplier.SupplierRepository,
|
||||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
expenseBridge PurchaseExpenseBridge,
|
expenseBridge PurchaseExpenseBridge,
|
||||||
) PurchaseService {
|
) PurchaseService {
|
||||||
return &purchaseService{
|
return &purchaseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
PurchaseRepo: purchaseRepo,
|
PurchaseRepo: purchaseRepo,
|
||||||
ProductRepo: productRepo,
|
ProductRepo: productRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
SupplierRepo: supplierRepo,
|
SupplierRepo: supplierRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
ExpenseBridge: expenseBridge,
|
ApprovalSvc: approvalSvc,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
ExpenseBridge: expenseBridge,
|
||||||
|
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
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
|
productId uint
|
||||||
warehouseId uint
|
warehouseId uint
|
||||||
subQty float64
|
subQty float64
|
||||||
|
pfkID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Items) == 0 {
|
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)
|
warehouseCache := make(map[uint]*entity.Warehouse)
|
||||||
productSupplierCache := make(map[uint]bool)
|
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 {
|
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 {
|
warehouse, err := s.WarehouseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("Area").Preload("Location")
|
return db.Preload("Area").Preload("Location")
|
||||||
@@ -239,21 +244,37 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
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)
|
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
|
warehouseCache[id] = warehouse
|
||||||
return warehouse, nil
|
return warehouse, pfkID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||||
indexMap := make(map[string]int)
|
indexMap := make(map[string]int)
|
||||||
|
|
||||||
for _, item := range req.Items {
|
for _, item := range req.Items {
|
||||||
if _, err := getWarehouse(item.WarehouseID); err != nil {
|
_, pfkID, err := getWarehouse(item.WarehouseID)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +303,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
productId: productId,
|
productId: productId,
|
||||||
warehouseId: warehouseId,
|
warehouseId: warehouseId,
|
||||||
subQty: item.Quantity,
|
subQty: item.Quantity,
|
||||||
|
pfkID: pfkID,
|
||||||
}
|
}
|
||||||
aggregated = append(aggregated, entry)
|
aggregated = append(aggregated, entry)
|
||||||
indexMap[key] = len(aggregated) - 1
|
indexMap[key] = len(aggregated) - 1
|
||||||
@@ -308,14 +330,15 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
emptyVehicle := ""
|
emptyVehicle := ""
|
||||||
for _, item := range aggregated {
|
for _, item := range aggregated {
|
||||||
items = append(items, &entity.PurchaseItem{
|
items = append(items, &entity.PurchaseItem{
|
||||||
ProductId: item.productId,
|
ProductId: item.productId,
|
||||||
WarehouseId: item.warehouseId,
|
WarehouseId: item.warehouseId,
|
||||||
SubQty: item.subQty,
|
ProjectFlockKandangId: item.pfkID,
|
||||||
TotalQty: 0,
|
SubQty: item.subQty,
|
||||||
TotalUsed: 0,
|
TotalQty: 0,
|
||||||
Price: 0,
|
TotalUsed: 0,
|
||||||
TotalPrice: 0,
|
Price: 0,
|
||||||
VehicleNumber: &emptyVehicle,
|
TotalPrice: 0,
|
||||||
|
VehicleNumber: &emptyVehicle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +355,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := purchaseRepoTx.BackfillProjectFlockKandang(c.Context(), purchase.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
actorID := uint(purchase.CreatedBy)
|
actorID := uint(purchase.CreatedBy)
|
||||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepPengajuan, entity.ApprovalActionCreated, actorID, nil, false); err != nil {
|
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepPengajuan, entity.ApprovalActionCreated, actorID, nil, false); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user