Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-390-Dashboard

This commit is contained in:
ragilap
2026-01-11 18:26:40 +07:00
59 changed files with 1349 additions and 332 deletions
@@ -1 +1,2 @@
DROP TABLE IF EXISTS expenses; DROP SEQUENCE IF EXISTS expenses_ref_seq;
DROP TABLE IF EXISTS expenses;
@@ -1,3 +1,3 @@
-- Drop function and sequence for sales order numbers -- Drop function and sequence for sales order numbers
DROP FUNCTION IF EXISTS generate_so_number();
DROP SEQUENCE IF EXISTS so_number_seq; DROP SEQUENCE IF EXISTS so_number_seq;
DROP FUNCTION IF EXISTS generate_so_number();
@@ -1,6 +1,8 @@
DROP TABLE IF EXISTS daily_checklist_tasks; -- Drop tables in correct order (child tables before parent tables)
DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees; DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
DROP TABLE IF EXISTS daily_checklist_activity_tasks; DROP TABLE IF EXISTS daily_checklist_activity_tasks;
DROP TABLE IF EXISTS daily_checklist_tasks;
DROP TABLE IF EXISTS daily_checklist_phases; DROP TABLE IF EXISTS daily_checklist_phases;
DROP TABLE IF EXISTS daily_checklists; DROP TABLE IF EXISTS daily_checklists;
DROP TABLE IF EXISTS checklists; DROP TABLE IF EXISTS checklists;
@@ -0,0 +1,4 @@
-- Remove expense_nonstock_id from stock_transfer_details
ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock;
ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id;
DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id;
@@ -0,0 +1,10 @@
-- Add expense_nonstock_id to stock_transfer_details
-- This allows tracking expedition/transport costs for stock transfers (same as purchase)
ALTER TABLE stock_transfer_details
ADD COLUMN expense_nonstock_id BIGINT,
ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL;
-- Create index for better query performance
CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id);
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
DROP COLUMN IF EXISTS party_account_number;
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
COMMIT;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1,20 @@
-- Revert master data foreign keys to CASCADE delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- Update master data foreign keys to RESTRICT delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,59 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
END IF;
END $$;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,3);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hand_day IS NULL OR hand_day >= 0) AND
(hand_house IS NULL OR hand_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mesh IS NULL OR egg_mesh >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
@@ -0,0 +1,57 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS daily_gain,
DROP COLUMN IF EXISTS avg_daily_gain;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hen_day IS NULL OR hen_day >= 0) AND
(hen_house IS NULL OR hen_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mass IS NULL OR egg_mass >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
+1
View File
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Tax: tax, Tax: tax,
ExpiryPeriod: seed.Expiry, ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy, CreatedBy: createdBy,
IsVisible: seed.IsVisible,
} }
if err := tx.Create(&product).Error; err != nil { if err := tx.Create(&product).Error; err != nil {
return err return err
+9 -9
View File
@@ -5,15 +5,15 @@ import (
) )
type ExpenseNonstock struct { type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""` ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""` ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""` KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""` NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;column:price"` Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
Notes string `gorm:"type:text;column:notes"` Notes string `gorm:"type:text;column:notes"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
+17 -16
View File
@@ -7,22 +7,23 @@ import (
) )
type Payment struct { type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"` PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"` ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"` TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"` PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"` PartyAccountNumber *string `gorm:"type:varchar(50)"`
PaymentMethod string `gorm:"type:varchar(20);not null"` PaymentDate time.Time `gorm:"not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"` PaymentMethod string `gorm:"type:varchar(20);not null"`
Direction string `gorm:"type:varchar(5);not null"` BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Nominal float64 `gorm:"type:numeric(15,3);not null"` Direction string `gorm:"type:varchar(5);not null"`
Notes string `gorm:"type:text;not null"` Nominal float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` Notes string `gorm:"type:text;not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"` BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+1 -1
View File
@@ -21,7 +21,7 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsVisible bool `gorm:"column:is_visible;default:true"` IsVisible bool ``
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
+6 -6
View File
@@ -16,10 +16,10 @@ type Recording struct {
CumIntake *int `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
HandDay *float64 `gorm:"column:hand_day"` HenDay *float64 `gorm:"column:hen_day"`
HandHouse *float64 `gorm:"column:hand_house"` HenHouse *float64 `gorm:"column:hen_house"`
FeedIntake *float64 `gorm:"column:feed_intake"` FeedIntake *float64 `gorm:"column:feed_intake"`
EggMesh *float64 `gorm:"column:egg_mesh"` EggMass *float64 `gorm:"column:egg_mass"`
EggWeight *float64 `gorm:"column:egg_weight"` EggWeight *float64 `gorm:"column:egg_weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -34,11 +34,11 @@ type Recording struct {
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
StandardHandDay *float64 `gorm:"-"` StandardHenDay *float64 `gorm:"-"`
StandardHandHouse *float64 `gorm:"-"` StandardHenHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"` StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"` StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMesh *float64 `gorm:"-"` StandardEggMass *float64 `gorm:"-"`
StandardEggWeight *float64 `gorm:"-"` StandardEggWeight *float64 `gorm:"-"`
StandardFcr *float64 `gorm:"-"` StandardFcr *float64 `gorm:"-"`
} }
+8 -13
View File
@@ -8,27 +8,22 @@ type StockTransferDetail struct {
StockTransferId uint64 StockTransferId uint64
ProductId uint64 ProductId uint64
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
// Tracking stock yang DIAMBIL dari source warehouse
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"` SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock) PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) === TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
// Tracking stock yang DITAMBAHKAN ke destination warehouse TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"` ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia CreatedAt time.Time
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === METADATA ===
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === RELATIONS === // === RELATIONS ===
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"` Product *Product `gorm:"foreignKey:ProductId"`
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"` SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"` DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"` DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
} }
@@ -247,7 +247,7 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
} }
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("projectFlockId")
projectFlockID, err := strconv.Atoi(param) projectFlockID, err := strconv.Atoi(param)
if err != nil { if err != nil {
@@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
kandang = &mapped kandang = &mapped
} }
var realizationDate time.Time
if e.DeliveryDate != nil {
realizationDate = *e.DeliveryDate
}
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id) doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{ return SalesDTO{
Id: e.Id, Id: e.Id,
RealizationDate: *e.DeliveryDate, RealizationDate: realizationDate,
Age: age, Age: age,
DoNumber: doNumber, DoNumber: doNumber,
Product: product, Product: product,
Customer: customer, Customer: customer,
Qty: e.UsageQty, // Show allocated quantity from FIFO Qty: e.UsageQty,
Weight: e.TotalWeight, Weight: e.TotalWeight,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
Price: e.UnitPrice, Price: e.UnitPrice,
@@ -10,6 +10,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -914,9 +915,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
var rows []ActualUsageCostRow var rows []ActualUsageCostRow
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll) purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
purchaseStockableKey := "PURCHASE_ITEMS" transferStockableKey := fifo.StockableKeyStockTransferIn.String()
transferStockableKey := "STOCK_TRANSFER_DETAILS"
recordingQuery := db. recordingQuery := db.
Table("recordings AS r"). Table("recordings AS r").
@@ -982,7 +982,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Part 2: Get usage from project_chickins (DOC, Pullet)
chickinQuery := db. chickinQuery := db.
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
@@ -1006,7 +1005,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Merge results
rows = append(rows, chickinRows...) rows = append(rows, chickinRows...)
return rows, nil return rows, nil
@@ -151,9 +151,19 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
return nil, err return nil, err
} }
if len(realisasi) == 0 { if len(realisasi) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found") return []entity.MarketingDeliveryProduct{}, nil
} }
return realisasi, nil
filtered := make([]entity.MarketingDeliveryProduct, 0, len(realisasi))
for _, item := range realisasi {
if item.UsageQty != 0 || item.TotalWeight != 0 || item.AvgWeight != 0 ||
item.UnitPrice != 0 || item.TotalPrice != 0 {
filtered = append(filtered, item)
}
}
return filtered, nil
} }
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
@@ -403,18 +413,9 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
} }
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return err == nil, err
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -429,13 +430,11 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
} }
// Get actual usage cost instead of purchase items
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
} }
// Convert actual usage rows to pseudo purchase items
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
@@ -9,7 +9,7 @@ type Create struct {
type Update struct { type Update struct {
Status string `json:"status" validate:"required"` Status string `json:"status" validate:"required"`
RejectReason *string `json:"reject_reason" validate:"required"` RejectReason *string `json:"reject_reason"`
} }
type Query struct { type Query struct {
@@ -101,20 +101,25 @@ func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
func partyFromInitial(e entity.Payment) Party { func partyFromInitial(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
@@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
TransactionType: string(utils.TransactionTypeSaldoAwal), TransactionType: string(utils.TransactionTypeSaldoAwal),
PartyType: party, PartyType: party,
PartyId: req.PartyId, PartyId: req.PartyId,
PartyAccountNumber: nil,
PaymentDate: time.Now(), PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
@@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
TransactionType: string(utils.TransactionTypeInjection), TransactionType: string(utils.TransactionTypeInjection),
PartyType: string(utils.PaymentPartyCustomer), PartyType: string(utils.PaymentPartyCustomer),
PartyId: 0, PartyId: 0,
PartyAccountNumber: nil,
PaymentDate: adjustmentDate, PaymentDate: adjustmentDate,
PaymentMethod: string(utils.PaymentMethodSaldo), PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId, BankId: req.BankId,
@@ -1,17 +1,17 @@
package validation package validation
type Create struct { type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"` BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"` AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"` Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"` Notes string `json:"notes" validate:"required_strict,max=500"`
} }
type Update struct { type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"` AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type Query struct { type Query struct {
@@ -124,20 +124,25 @@ func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
func partyFromPayment(e entity.Payment) Party { func partyFromPayment(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
+3 -3
View File
@@ -15,7 +15,7 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService
route := v1.Group("/payments") route := v1.Group("/payments")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne) route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
} }
@@ -121,18 +121,19 @@ func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
createBody := &entity.Payment{ createBody := &entity.Payment{
PaymentCode: code, PaymentCode: code,
ReferenceNumber: req.ReferenceNumber, ReferenceNumber: req.ReferenceNumber,
TransactionType: transactionType, TransactionType: transactionType,
PartyType: party, PartyType: party,
PartyId: req.PartyId, PartyId: req.PartyId,
PaymentDate: paymentDate, PartyAccountNumber: req.PartyAccountNumber,
PaymentMethod: method, PaymentDate: paymentDate,
BankId: req.BankId, PaymentMethod: method,
Direction: directionForParty(party), BankId: req.BankId,
Nominal: req.Nominal, Direction: directionForParty(party),
Notes: req.Notes, Nominal: req.Nominal,
CreatedBy: actorID, Notes: req.Notes,
CreatedBy: actorID,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -188,6 +189,9 @@ func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.ReferenceNumber != nil { if req.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber updateBody["reference_number"] = *req.ReferenceNumber
} }
if req.PartyAccountNumber != nil {
updateBody["party_account_number"] = *req.PartyAccountNumber
}
if req.PaymentMethod != nil { if req.PaymentMethod != nil {
method, err := normalizePaymentMethod(*req.PaymentMethod) method, err := normalizePaymentMethod(*req.PaymentMethod)
if err != nil { if err != nil {
@@ -1,25 +1,27 @@
package validation package validation
type Create struct { type Create struct {
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"` PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"` PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"` PartyAccountNumber *string `json:"party_account_number"`
Nominal float64 `json:"nominal" validate:"required_strict"` PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
ReferenceNumber *string `json:"reference_number,omitempty"` Nominal float64 `json:"nominal" validate:"required_strict"`
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"` ReferenceNumber *string `json:"reference_number,omitempty"`
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"` PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
Notes string `json:"notes" validate:"required_strict,max=500"` BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
} }
type Update struct { type Update struct {
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"` PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"` PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"` PartyAccountNumber *string `json:"party_account_number,omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"` PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
ReferenceNumber *string `json:"reference_number,omitempty"` Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"` ReferenceNumber *string `json:"reference_number,omitempty"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"` PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type Query struct { type Query struct {
@@ -124,20 +124,25 @@ func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
func partyFromPayment(e entity.Payment) Party { func partyFromPayment(e entity.Payment) Party {
party := Party{ party := Party{
Id: e.PartyId, Id: e.PartyId,
Type: e.PartyType, Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
} }
switch utils.PaymentParty(e.PartyType) { switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer: case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 { if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
} }
case utils.PaymentPartySupplier: case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil { if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber party.AccountNumber = *e.Supplier.AccountNumber
} }
} }
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
@@ -380,3 +381,38 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Conte
} }
return &product, nil return &product, nil
} }
func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) {
if len(prefixes) == 0 {
return []uint{}, nil
}
db := r.DB().WithContext(ctx).
Model(&entity.Product{}).
Distinct("products.id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct)
applied := false
for _, prefix := range prefixes {
if prefix == "" {
continue
}
like := prefix + "%"
if !applied {
db = db.Where("flags.name LIKE ?", like)
applied = true
continue
}
db = db.Or("flags.name LIKE ?", like)
}
if visibleStatus != nil {
db = db.Where("products.is_visible = ?", *visibleStatus)
}
var ids []uint
if err := db.Pluck("products.id", &ids).Error; err != nil {
return nil, err
}
return ids, nil
}
@@ -75,6 +75,8 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
func (u *TransferController) CreateOne(c *fiber.Ctx) error { func (u *TransferController) CreateOne(c *fiber.Ctx) error {
data := c.FormValue("data") data := c.FormValue("data")
const maxFileSize = 5 * 1024 * 1024
var req validation.TransferRequest var req validation.TransferRequest
if err := json.Unmarshal([]byte(data), &req); err != nil { if err := json.Unmarshal([]byte(data), &req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
@@ -87,9 +89,11 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
files := form.File["documents"] files := form.File["documents"]
if len(files) != len(req.Deliveries) { for i, file := range files {
return fiber.NewError(fiber.StatusBadRequest, if file.Size > maxFileSize {
fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message) return fiber.NewError(fiber.StatusBadRequest,
"Dokumen ke-"+strconv.Itoa(i+1)+" melebihi ukuran maksimal 5MB")
}
} }
result, err := u.TransferService.CreateOne(c, &req, files) result, err := u.TransferService.CreateOne(c, &req, files)
@@ -71,9 +71,11 @@ type TransferDetailDTO struct {
} }
type TransferDetailItemDTO struct { type TransferDetailItemDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Product ProductSimpleDTO `json:"product"` Product ProductSimpleDTO `json:"product"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
TransportPerItem *float64 `json:"transport_per_item,omitempty"` // Biaya ekspedisi per item
ExpeditionVendor *SupplierSimpleDTO `json:"expedition_vendor,omitempty"` // Vendor ekspedisi
} }
type TransferDeliveryDTO struct { type TransferDeliveryDTO struct {
@@ -153,14 +155,30 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var details []TransferDetailItemDTO var details []TransferDetailItemDTO
for _, d := range e.Details { for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{ detailDTO := TransferDetailItemDTO{
Id: d.Id, Id: d.Id,
Product: ProductSimpleDTO{ Product: ProductSimpleDTO{
Id: d.Product.Id, Id: d.Product.Id,
Name: d.Product.Name, Name: d.Product.Name,
}, },
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
}) }
if d.ExpenseNonstock != nil {
priceCopy := d.ExpenseNonstock.Price
detailDTO.TransportPerItem = &priceCopy
if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 {
exp := d.ExpenseNonstock.Expense
detailDTO.ExpeditionVendor = &SupplierSimpleDTO{
Id: exp.Supplier.Id,
Name: exp.Supplier.Name,
}
}
}
details = append(details, detailDTO)
} }
var deliveries []TransferDeliveryDTO var deliveries []TransferDeliveryDTO
@@ -223,14 +241,30 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
var details []TransferDetailItemDTO var details []TransferDetailItemDTO
for _, d := range e.Details { for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{ detailDTO := TransferDetailItemDTO{
Id: d.Id, Id: d.Id,
Product: ProductSimpleDTO{ Product: ProductSimpleDTO{
Id: d.Product.Id, Id: d.Product.Id,
Name: d.Product.Name, Name: d.Product.Name,
}, },
Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated Quantity: d.UsageQty + d.PendingQty, // Total actual quantity allocated
}) }
if d.ExpenseNonstock != nil {
priceCopy := d.ExpenseNonstock.Price
detailDTO.TransportPerItem = &priceCopy
if d.ExpenseNonstock.Expense != nil && d.ExpenseNonstock.Expense.Supplier != nil && d.ExpenseNonstock.Expense.Supplier.Id != 0 {
exp := d.ExpenseNonstock.Expense
detailDTO.ExpeditionVendor = &SupplierSimpleDTO{
Id: exp.Supplier.Id,
Name: exp.Supplier.Name,
}
}
}
details = append(details, detailDTO)
} }
var deliveries []TransferDeliveryDTO var deliveries []TransferDeliveryDTO
+35 -1
View File
@@ -2,6 +2,7 @@ package transfers
import ( import (
"context" "context"
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -9,9 +10,13 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
expenseService "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/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"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -35,15 +40,44 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
nonstockRepo := rNonstock.NewNonstockRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db) documentRepo := commonRepo.NewDocumentRepository(db)
stockAllocRepo := commonRepo.NewStockAllocationRepository(db) stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
expenseRepository := expenseRepo.NewExpenseRepository(db)
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo) documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil { if err != nil {
panic(err) panic(err)
} }
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
}
expenseServiceInstance := expenseService.NewExpenseService(
expenseRepository,
supplierRepo,
nonstockRepo,
approvalSvc,
expenseRealizationRepo,
projectFlockKandangRepo,
documentSvc,
validate,
)
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
expenseBridge := sTransfer.NewTransferExpenseBridge(
db,
stockTransferRepo,
projectFlockKandangRepo,
kandangRepo,
expenseServiceInstance,
)
err = fifoService.RegisterStockable(fifo.StockableConfig{ err = fifoService.RegisterStockable(fifo.StockableConfig{
Key: fifo.StockableKeyStockTransferIn, Key: fifo.StockableKeyStockTransferIn,
@@ -77,7 +111,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
panic(err) panic(err)
} }
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService) transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, expenseBridge)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService) TransferRoutes(router, userService, transferService)
@@ -2,6 +2,7 @@ package repositories
import ( import (
"context" "context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -12,6 +13,7 @@ type StockTransferRepository interface {
repository.BaseRepository[entity.StockTransfer] repository.BaseRepository[entity.StockTransfer]
// get sequence for movement number // get sequence for movement number
GetNextMovementNumber(ctx context.Context) (int64, error) GetNextMovementNumber(ctx context.Context) (int64, error)
GenerateMovementNumber(ctx context.Context) (string, error)
} }
type StockTransferRepositoryImpl struct { type StockTransferRepositoryImpl struct {
@@ -32,3 +34,12 @@ func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context)
} }
return seq, nil return seq, nil
} }
func (r *StockTransferRepositoryImpl) GenerateMovementNumber(ctx context.Context) (string, error) {
seq, err := r.GetNextMovementNumber(ctx)
if err != nil {
return "", err
}
movementNumber := fmt.Sprintf("ST-%05d", seq)
return movementNumber, nil
}
@@ -46,9 +46,10 @@ type transferService struct {
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
DocumentSvc commonSvc.DocumentService DocumentSvc commonSvc.DocumentService
FifoSvc commonSvc.FifoService FifoSvc commonSvc.FifoService
ExpenseBridge TransferExpenseBridge
} }
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService) TransferService { func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, expenseBridge TransferExpenseBridge) TransferService {
return &transferService{ return &transferService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -63,6 +64,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
ProjectFlockKandangRepo: projectFlockKandangRepo, ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc, DocumentSvc: documentSvc,
FifoSvc: fifoSvc, FifoSvc: fifoSvc,
ExpenseBridge: expenseBridge,
} }
} }
@@ -77,6 +79,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
Preload("ToWarehouse.Area"). Preload("ToWarehouse.Area").
Preload("Details"). Preload("Details").
Preload("Details.Product"). Preload("Details.Product").
Preload("Details.ExpenseNonstock").
Preload("Details.ExpenseNonstock.Expense").
Preload("Details.ExpenseNonstock.Expense.Supplier").
Preload("Deliveries.Items"). Preload("Deliveries.Items").
Preload("Deliveries.Supplier"). Preload("Deliveries.Supplier").
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB { Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
@@ -123,7 +128,6 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) { func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
// === VALIDASI SOURCE WAREHOUSE ===
pwIDs := make([]uint, 0, len(req.Products)) pwIDs := make([]uint, 0, len(req.Products))
for _, product := range req.Products { for _, product := range req.Products {
@@ -155,14 +159,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return nil, err return nil, err
} }
if s.ProjectFlockKandangRepo != nil { projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID) if err != nil {
if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock") }
} if projectFlockKandang.ClosedAt != nil {
if projectFlockKandang.ClosedAt != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
}
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := m.ActorIDFromContext(c)
@@ -192,16 +194,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier") return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
} }
if supplier.Category != "BOP" { if supplier.Category != string(utils.SupplierCategoryBOP) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
} }
} }
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context()) movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
} }
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
transferDate, _ := utils.ParseDateString(req.TransferDate) transferDate, _ := utils.ParseDateString(req.TransferDate)
entityTransfer := &entity.StockTransfer{ entityTransfer := &entity.StockTransfer{
@@ -213,19 +215,26 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
CreatedBy: uint64(actorID), CreatedBy: uint64(actorID),
} }
expensePayloads := make([]TransferExpenseReceivingPayload, 0)
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil { stockTransferRepoTX := s.StockTransferRepo.WithTx(tx)
stockTransferDetailRepoTX := s.StockTransferDetailRepo.WithTx(tx)
stockTransferDeliveryRepoTX := s.StockTransferDeliveryRepo.WithTx(tx)
stockTransferDeliveryItemRepoTX := s.StockTransferDeliveryItemRepo.WithTx(tx)
productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
if err := stockTransferRepoTX.CreateOne(c.Context(), entityTransfer, nil); err != nil {
return err return err
} }
// Prepare details and fetch product warehouses
details := make([]*entity.StockTransferDetail, 0, len(req.Products)) details := make([]*entity.StockTransferDetail, 0, len(req.Products))
detailMap := make(map[uint64]*entity.StockTransferDetail) detailMap := make(map[uint64]*entity.StockTransferDetail)
for _, product := range req.Products { for _, product := range req.Products {
// Get source product warehouse
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID( sourcePW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID), c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
) )
if err != nil { if err != nil {
@@ -235,8 +244,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source") return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
} }
// Get or create destination product warehouse destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID), c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
) )
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -254,7 +262,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Quantity: 0, Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID, ProjectFlockKandangId: &projectFlockKandangID,
} }
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil { if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination") return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
} }
} }
@@ -275,7 +283,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
detailMap[uint64(product.ProductID)] = detail detailMap[uint64(product.ProductID)] = detail
} }
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil { if err := stockTransferDetailRepoTX.CreateMany(c.Context(), details, nil); err != nil {
return err return err
} }
@@ -290,7 +298,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ShippingCostTotal: delivery.DeliveryCost, ShippingCostTotal: delivery.DeliveryCost,
}) })
} }
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil { if err := stockTransferDeliveryRepoTX.CreateMany(c.Context(), deliveries, nil); err != nil {
return err return err
} }
@@ -310,30 +318,44 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
} }
} }
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil { if err := stockTransferDeliveryItemRepoTX.CreateMany(c.Context(), deliveryItems, nil); err != nil {
return err return err
} }
if s.DocumentSvc != nil && len(files) > 0 { if s.DocumentSvc != nil && len(files) > 0 {
for idx, file := range files { for deliveryIdx, delivery := range deliveries {
reqDelivery := req.Deliveries[deliveryIdx]
if reqDelivery.DocumentIndex < 0 {
continue
}
if reqDelivery.DocumentIndex >= len(files) {
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("DocumentIndex %d untuk delivery %d melebihi jumlah file yang diupload (%d)",
reqDelivery.DocumentIndex, deliveryIdx+1, len(files)))
}
file := files[reqDelivery.DocumentIndex]
documentFiles := []commonSvc.DocumentFile{ documentFiles := []commonSvc.DocumentFile{
{ {
File: file, File: file,
Type: string(utils.DocumentTypeTransfer), Type: string(utils.DocumentTypeTransfer),
Index: &idx, Index: &reqDelivery.DocumentIndex,
}, },
} }
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{ _, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeTransfer), DocumentableType: string(utils.DocumentableTypeTransfer),
DocumentableID: deliveries[idx].Id, DocumentableID: delivery.Id,
CreatedBy: &actorID, CreatedBy: &actorID,
Files: documentFiles, Files: documentFiles,
}) })
if err != nil { if err != nil {
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)", s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
idx+1, deliveries[idx].Id, file.Filename) deliveryIdx+1, delivery.Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err))
} }
} }
} }
@@ -362,7 +384,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return fmt.Errorf("gagal update usage tracking: %w", err) return fmt.Errorf("gagal update usage tracking: %w", err)
} }
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber) note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyStockTransferIn, StockableKey: fifo.StockableKeyStockTransferIn,
@@ -385,6 +406,32 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
} }
} }
if len(req.Deliveries) > 0 {
for _, delivery := range req.Deliveries {
for _, prod := range delivery.Products {
detail := detailMap[uint64(prod.ProductID)]
if detail == nil {
continue
}
warehouseID := uint(req.DestinationWarehouseID)
supplierID := uint(delivery.SupplierID)
deliveredDate := transferDate
deliveredQty := prod.ProductQty
payload := TransferExpenseReceivingPayload{
TransferDetailID: detail.Id,
ProductID: uint64(prod.ProductID),
WarehouseID: uint64(warehouseID),
SupplierID: uint64(supplierID),
DeliveredQty: deliveredQty,
DeliveredDate: &deliveredDate,
}
expensePayloads = append(expensePayloads, payload)
}
}
}
return nil return nil
}) })
@@ -396,9 +443,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(expensePayloads) > 0 {
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
}
}
return result, nil return result, nil
} }
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
if s.ExpenseBridge == nil || transferID == 0 || len(payloads) == 0 {
return nil
}
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
}
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
return nil
}
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
}
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
if err != nil { if err != nil {
@@ -0,0 +1,473 @@
package service
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type TransferExpenseBridge interface {
OnItemsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error
OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error
}
type TransferExpenseReceivingPayload struct {
TransferDetailID uint64
ProductID uint64
WarehouseID uint64
SupplierID uint64
TransportPerItem *float64
DeliveredQty float64
DeliveredDate *time.Time
}
type groupedTransferItem struct {
detail *entity.StockTransferDetail
payload TransferExpenseReceivingPayload
projectFK *uint
kandangID *uint
totalPrice float64
shippingCostTotal float64
}
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
}
type transferExpenseBridge struct {
db *gorm.DB
transferRepo rStockTransfer.StockTransferRepository
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
kandangRepo kandangRepo.KandangRepository
expenseSvc expenseSvc.ExpenseService
}
func NewTransferExpenseBridge(
db *gorm.DB,
transferRepo rStockTransfer.StockTransferRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
kandangRepo kandangRepo.KandangRepository,
expenseSvc expenseSvc.ExpenseService,
) TransferExpenseBridge {
return &transferExpenseBridge{
db: db,
transferRepo: transferRepo,
projectFlockKandangRepo: projectFlockKandangRepo,
kandangRepo: kandangRepo,
expenseSvc: expenseSvc,
}
}
func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, items []entity.StockTransferDetail) error {
if len(items) == 0 {
return nil
}
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
expenseIDs := make(map[uint64]struct{})
expenseNonstockIDs := make([]uint64, 0)
for _, item := range items {
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
}
}
if len(expenseNonstockIDs) > 0 {
for _, nsID := range expenseNonstockIDs {
var expenseID uint64
if err := tx.Model(&entity.ExpenseNonstock{}).
Select("expense_id").
Where("id = ?", nsID).
Scan(&expenseID).Error; err != nil {
return err
}
if expenseID != 0 {
expenseIDs[expenseID] = struct{}{}
}
}
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
return err
}
}
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
for expenseID := range expenseIDs {
var count int64
if err := tx.Model(&entity.ExpenseNonstock{}).
Where("expense_id = ?", expenseID).
Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
return err
}
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
return err
}
}
}
return nil
})
}
func (b *transferExpenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
if len(expenseIDs) == 0 {
return nil
}
if actorID == 0 {
actorID = 1
}
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
action := entity.ApprovalActionUpdated
for id := range expenseIDs {
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return err
}
}
return nil
}
func (b *transferExpenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
var id uint64
err := b.db.WithContext(ctx).
Table("nonstocks AS ns").
Select("ns.id").
Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
Where("nss.supplier_id = ?", supplierID).
Order("ns.id").
Limit(1).
Scan(&id).Error
if err != nil {
return 0, err
}
if id == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
}
return id, nil
}
func (b *transferExpenseBridge) createExpenseViaService(
c *fiber.Ctx,
transfer *entity.StockTransfer,
items []groupedTransferItem,
expenseDate time.Time,
expeditionNonstockID uint64,
movementNumber string,
supplierID uint,
) (*expenseDto.ExpenseDetailDTO, error) {
ctx := c.Context()
if b.expenseSvc == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
}
if len(items) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
}
kandangID := items[0].kandangID
var locationID uint64
var expenseKandangID *uint64
if kandangID != nil && *kandangID != 0 {
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
return db.Select("id, location_id")
})
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
}
if kandang == nil {
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
}
locationID = uint64(kandang.LocationId)
id := uint64(*kandangID)
expenseKandangID = &id
} else {
if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense")
}
locationID = uint64(*transfer.ToWarehouse.LocationId)
}
costItems := make([]expenseValidation.CostItem, 0, len(items))
for _, gi := range items {
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
price := gi.shippingCostTotal
if gi.payload.TransportPerItem != nil {
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
}
costItems = append(costItems, expenseValidation.CostItem{
NonstockID: expeditionNonstockID,
Quantity: 1,
Price: price,
Notes: note,
})
}
req := &expenseValidation.Create{
PoNumber: "",
TransactionDate: utils.FormatDate(expenseDate),
Category: string(utils.ExpenseCategoryBOP),
SupplierID: uint64(supplierID),
LocationID: locationID,
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
KandangID: expenseKandangID,
CostItems: costItems,
}},
}
detail, err := b.expenseSvc.CreateOne(c, req)
if err != nil {
return nil, err
}
action := entity.ApprovalActionApproved
actorID := uint(transfer.CreatedBy)
if actorID == 0 {
actorID = 1
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
return nil, err
}
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
return nil, err
}
return detail, nil
}
func (b *transferExpenseBridge) linkExpenseNonstocksToDetails(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedTransferItem) error {
if detail == nil || len(items) == 0 {
return nil
}
noteToExpenseNonstock := mapExpenseNotesForTransfer(detail)
if len(noteToExpenseNonstock) == 0 {
return nil
}
for _, gi := range items {
expenseNonstockID, ok := noteToExpenseNonstock[gi.detail.Id]
if !ok {
continue
}
if err := b.db.WithContext(ctx).
Model(&entity.StockTransferDetail{}).
Where("id = ?", gi.detail.Id).
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
return err
}
}
return nil
}
func mapExpenseNotesForTransfer(detail *expenseDto.ExpenseDetailDTO) map[uint64]uint64 {
result := make(map[uint64]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 = "stock_transfer_detail:"
if !strings.HasPrefix(note, prefix) {
continue
}
idStr := strings.TrimPrefix(note, prefix)
var detailID uint64
if _, err := fmt.Sscanf(idStr, "%d", &detailID); err != nil {
continue
}
result[detailID] = pengajuan.Id
}
}
return result
}
func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error {
if transferID == 0 || len(updates) == 0 {
return nil
}
ctx := c.Context()
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
return db.
Preload("Details").
Preload("Details.Product").
Preload("Details.DestProductWarehouse").
Preload("Details.DeliveryItems").
Preload("Details.DeliveryItems.StockTransferDelivery").
Preload("ToWarehouse")
})
if err != nil {
return err
}
detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details))
shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal
for i := range transfer.Details {
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
if deliveryItem.StockTransferDelivery != nil {
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
break
}
}
}
groups := make(map[string][]groupedTransferItem)
for _, payload := range updates {
if payload.DeliveredDate == nil {
return fiber.NewError(fiber.StatusBadRequest, "delivered_date is required")
}
detail := detailMap[payload.TransferDetailID]
if detail == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer detail %d not found", payload.TransferDetailID))
}
if payload.DeliveredQty <= 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Delivered quantity for detail %d must be greater than 0", payload.TransferDetailID))
}
deliveredDate := payload.DeliveredDate.UTC().Truncate(24 * time.Hour)
supplierID := payload.SupplierID
if supplierID == 0 {
supplierID = 1 // Default supplier
}
var kandangID *uint
var projectFK *uint
if detail.DestProductWarehouse.WarehouseId != 0 {
var kandangIDResult uint
if err := b.db.WithContext(ctx).
Table("warehouses").
Select("kandang_id").
Where("id = ?", detail.DestProductWarehouse.WarehouseId).
Scan(&kandangIDResult).Error; err == nil && kandangIDResult != 0 {
id := uint(kandangIDResult)
kandangID = &id
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, kandangIDResult); err == nil && project != nil {
pid := uint(project.Id)
projectFK = &pid
}
}
}
shippingCostTotal := shippingCostMap[detail.Id]
totalPrice := shippingCostTotal
if payload.TransportPerItem != nil {
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
}
warehouseID := uint(payload.WarehouseID)
if warehouseID == 0 && transfer.ToWarehouse != nil {
warehouseID = uint(transfer.ToWarehouse.Id)
}
if warehouseID == 0 && detail.DestProductWarehouse != nil {
warehouseID = uint(detail.DestProductWarehouse.WarehouseId)
}
key := groupingKey(uint(supplierID), deliveredDate, warehouseID)
groups[key] = append(groups[key], groupedTransferItem{
detail: detail,
payload: payload,
projectFK: projectFK,
kandangID: kandangID,
totalPrice: totalPrice,
shippingCostTotal: shippingCostTotal,
})
}
updatedExpenses := make(map[uint64]struct{})
for key, items := range groups {
if len(items) == 0 {
continue
}
parts := strings.Split(key, ":")
if len(parts) < 3 {
return errors.New("invalid expense grouping key")
}
expenseDate, err := utils.ParseDateString(parts[1])
if err != nil {
return err
}
supplierID, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return err
}
expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
if err != nil {
return err
}
expenseDetail, err := b.createExpenseViaService(c, transfer, items, expenseDate, expeditionNonstockID, transfer.MovementNumber, uint(supplierID))
if err != nil {
return err
}
if err := b.linkExpenseNonstocksToDetails(ctx, expenseDetail, items); err != nil {
return err
}
if expenseDetail != nil && expenseDetail.Id != 0 {
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
}
}
if len(updatedExpenses) > 0 {
actorID := uint(1) // Default actor
if err := b.markExpensesUpdated(ctx, updatedExpenses, actorID); err != nil {
return err
}
}
return nil
}
@@ -23,7 +23,7 @@ type TransferDeliveryProduct struct {
type TransferDelivery struct { type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost" validate:"required"` DeliveryCost float64 `json:"delivery_cost" validate:"required"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"` DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
DocumentIndex int `json:"document_index" validate:"min=0"` DocumentIndex int `json:"document_index" validate:"omitempty,min=-1" default:"-1"`
DriverName string `json:"driver_name" validate:"required"` DriverName string `json:"driver_name" validate:"required"`
VehiclePlate string `json:"vehicle_plate" validate:"required"` VehiclePlate string `json:"vehicle_plate" validate:"required"`
SupplierID uint `json:"supplier_id" validate:"required"` SupplierID uint `json:"supplier_id" validate:"required"`
@@ -14,7 +14,7 @@ type Update struct {
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=500,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
KandangId *uint `query:"kandang_id" validate:"omitempty"` KandangId *uint `query:"kandang_id" validate:"omitempty"`
IsActive *bool `query:"is_active" validate:"omitempty"` IsActive *bool `query:"is_active" validate:"omitempty"`
@@ -28,20 +28,12 @@ func (u *PhaseActivityController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
query.PhaseIDs = c.Query("phase_ids", "")
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
} }
if phaseParam := c.Query("phase_id", ""); phaseParam != "" {
id, err := strconv.Atoi(phaseParam)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "invalid phase_id")
}
temp := uint(id)
query.PhaseId = &temp
}
result, totalResults, err := u.PhaseActivityService.GetAll(c, query) result, totalResults, err := u.PhaseActivityService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -2,6 +2,7 @@ package service
import ( import (
"errors" "errors"
"strconv"
"strings" "strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -57,8 +58,11 @@ func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]
if params.Search != "" { if params.Search != "" {
db = db.Where("name LIKE ?", "%"+params.Search+"%") db = db.Where("name LIKE ?", "%"+params.Search+"%")
} }
if params.PhaseId != nil { if params.PhaseIDs != "" {
db = db.Where("phase_id = ?", *params.PhaseId) ids := parseIDs(params.PhaseIDs)
if len(ids) > 0 {
db = db.Where("phase_id IN ?", ids)
}
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -166,3 +170,18 @@ func (s phaseActivityService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
return nil return nil
} }
func parseIDs(raw string) []uint {
parts := strings.Split(raw, ",")
results := make([]uint, 0, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if n, err := strconv.ParseUint(value, 10, 64); err == nil {
results = append(results, uint(n))
}
}
return results
}
@@ -14,8 +14,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
PhaseId *uint `query:"phase_id" validate:"omitempty"` PhaseIDs string `query:"phase_ids" validate:"omitempty"`
} }
@@ -5,6 +5,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -19,6 +20,7 @@ type ProductRelationDTO struct {
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
} }
type ProductListDTO struct { type ProductListDTO struct {
@@ -33,6 +35,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -70,6 +73,7 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Flags: &flags, Flags: &flags,
Uom: uomRef, Uom: uomRef,
ProductCategory: categoryRef, ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
} }
} }
@@ -112,6 +116,7 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
ProductCategory: categoryRef, ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
} }
} }
@@ -128,3 +133,23 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
ProductListDTO: ToProductListDTO(e), ProductListDTO: ToProductListDTO(e),
} }
} }
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
}
if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
return result
}
@@ -20,6 +20,7 @@ type ProjectflockRepository interface {
GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error)
GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error)
GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error)
IdExists(ctx context.Context, id uint) (bool, error)
AreaExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error)
ProductionStandardExists(ctx context.Context, id uint) (bool, error) ProductionStandardExists(ctx context.Context, id uint) (bool, error)
@@ -161,6 +162,10 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
) )
} }
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) { func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Area](ctx, r.DB(), id) return repository.Exists[entity.Area](ctx, r.DB(), id)
} }
@@ -23,6 +23,7 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -308,12 +309,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
} }
createBody := &entity.ProjectFlock{ createBody := &entity.ProjectFlock{
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId, ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: actorID, CreatedBy: actorID,
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -823,22 +824,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs) // NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist.
if err != nil {
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
}
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs) pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
if err != nil { if err != nil {
@@ -854,6 +840,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang") return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
} }
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
purchaseRepo := purchaseRepository.NewPurchaseRepository(db)
if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang")
}
pwRepo := s.ProductWarehouseRepo pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil { if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -906,6 +900,11 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return nil return nil
} }
projectFlockID := records[0].ProjectFlockId
if projectFlockID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan")
}
pwRepo := s.ProductWarehouseRepo pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil { if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -920,24 +919,34 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB()) warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
} }
flags := []utils.FlagType{ db := s.Repository.DB()
utils.FlagAyamAfkir, if dbTransaction != nil {
utils.FlagAyamCulling, db = dbTransaction
utils.FlagAyamMati, }
utils.FlagTelurPecah, var category string
utils.FlagTelurUtuh, if err := db.WithContext(ctx).
Model(&entity.ProjectFlock{}).
Select("category").
Where("id = ?", projectFlockID).
Scan(&category).Error; err != nil {
return err
}
if strings.TrimSpace(category) == "" {
return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan")
} }
productIDs := make(map[utils.FlagType]uint, len(flags)) prefixes := []string{"AYAM-"}
for _, flag := range flags { if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag)) prefixes = append(prefixes, "TELUR")
if err != nil { }
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag)) invisibleOnly := false
} productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly)
return err if err != nil {
} return err
productIDs[flag] = product.Id }
if len(productIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", ")))
} }
for _, record := range records { for _, record := range records {
@@ -953,8 +962,7 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return err return err
} }
for _, flag := range flags { for _, productID := range productIDs {
productID := productIDs[flag]
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil { if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
continue continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -25,16 +25,16 @@ type RecordingRelationDTO struct {
CumIntake int `json:"cum_intake"` CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"` FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"` TotalChickQty float64 `json:"total_chick_qty"`
HandDay float64 `json:"hand_day"` HenDay float64 `json:"hen_day"`
HandHouse float64 `json:"hand_house"` HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"` FeedIntake float64 `json:"feed_intake"`
EggMesh float64 `json:"egg_mesh"` EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"` EggWeight float64 `json:"egg_weight"`
StandardHandDay *float64 `json:"hand_day_std,omitempty"` StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHandHouse *float64 `json:"hand_house_std,omitempty"` StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"` StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"` StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"` StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"` StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"` StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
@@ -94,10 +94,10 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
cumIntake int cumIntake int
fcrValue float64 fcrValue float64
totalChickQty float64 totalChickQty float64
handDay float64 henDay float64
handHouse float64 henHouse float64
feedIntake float64 feedIntake float64
eggMesh float64 eggMass float64
eggWeight float64 eggWeight float64
) )
@@ -119,17 +119,17 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
if e.TotalChickQty != nil { if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty totalChickQty = *e.TotalChickQty
} }
if e.HandDay != nil { if e.HenDay != nil {
handDay = *e.HandDay henDay = *e.HenDay
} }
if e.HandHouse != nil { if e.HenHouse != nil {
handHouse = *e.HandHouse henHouse = *e.HenHouse
} }
if e.FeedIntake != nil { if e.FeedIntake != nil {
feedIntake = *e.FeedIntake feedIntake = *e.FeedIntake
} }
if e.EggMesh != nil { if e.EggMass != nil {
eggMesh = *e.EggMesh eggMass = *e.EggMass
} }
if e.EggWeight != nil { if e.EggWeight != nil {
eggWeight = *e.EggWeight eggWeight = *e.EggWeight
@@ -157,16 +157,16 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
CumIntake: cumIntake, CumIntake: cumIntake,
FcrValue: fcrValue, FcrValue: fcrValue,
TotalChickQty: totalChickQty, TotalChickQty: totalChickQty,
HandDay: handDay, HenDay: henDay,
HandHouse: handHouse, HenHouse: henHouse,
FeedIntake: feedIntake, FeedIntake: feedIntake,
EggMesh: eggMesh, EggMass: eggMass,
EggWeight: eggWeight, EggWeight: eggWeight,
StandardHandDay: e.StandardHandDay, StandardHenDay: e.StandardHenDay,
StandardHandHouse: e.StandardHandHouse, StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake, StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion, StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMesh: e.StandardEggMesh, StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight, StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr, StandardFcr: e.StandardFcr,
Approval: latestApproval, Approval: latestApproval,
@@ -16,10 +16,10 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll) route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne) route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne)
route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne) route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne)
route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne) route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne) route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve) route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve)
} }
@@ -901,47 +901,6 @@ type eggTotals struct {
Weight float64 Weight float64
} }
type stockTotals struct {
Usage float64
Pending float64
Total float64
}
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += usage
current.Pending += pending
current.Total += usage + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += stock.Qty
current.Pending += pending
current.Total += stock.Qty + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false hasPending := false
@@ -1156,34 +1115,34 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
recording.FeedIntake = nil recording.FeedIntake = nil
} }
var handDay float64 var henDay float64
if remainingChick > 0 && totalEggQty >= 0 { if remainingChick > 0 && totalEggQty >= 0 {
handDay = (totalEggQty / remainingChick) * 100 henDay = (totalEggQty / remainingChick) * 100
updates["hand_day"] = handDay updates["hen_day"] = henDay
recording.HandDay = &handDay recording.HenDay = &henDay
} else { } else {
updates["hand_day"] = gorm.Expr("NULL") updates["hen_day"] = gorm.Expr("NULL")
recording.HandDay = nil recording.HenDay = nil
} }
var handHouse float64 var henHouse float64
if initialChickin > 0 && cumulativeEggQty >= 0 { if initialChickin > 0 && cumulativeEggQty >= 0 {
handHouse = cumulativeEggQty / initialChickin henHouse = cumulativeEggQty / initialChickin
updates["hand_house"] = handHouse updates["hen_house"] = henHouse
recording.HandHouse = &handHouse recording.HenHouse = &henHouse
} else { } else {
updates["hand_house"] = gorm.Expr("NULL") updates["hen_house"] = gorm.Expr("NULL")
recording.HandHouse = nil recording.HenHouse = nil
} }
var eggMesh float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMesh = (totalEggWeightGrams / remainingChick) * 1000 eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mesh"] = eggMesh updates["egg_mass"] = eggMass
recording.EggMesh = &eggMesh recording.EggMass = &eggMass
} else { } else {
updates["egg_mesh"] = gorm.Expr("NULL") updates["egg_mass"] = gorm.Expr("NULL")
recording.EggMesh = nil recording.EggMass = nil
} }
var eggWeight float64 var eggWeight float64
@@ -1334,11 +1293,11 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit
} }
type productionStandardValues struct { type productionStandardValues struct {
HandDay *float64 HenDay *float64
HandHouse *float64 HenHouse *float64
FeedIntake *float64 FeedIntake *float64
MaxDepletion *float64 MaxDepletion *float64
EggMesh *float64 EggMass *float64
EggWeight *float64 EggWeight *float64
} }
@@ -1389,10 +1348,10 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
return err return err
} }
if detail != nil { if detail != nil {
standard.HandDay = detail.TargetHenDayProduction standard.HenDay = detail.TargetHenDayProduction
standard.HandHouse = detail.TargetHenHouseProduction standard.HenHouse = detail.TargetHenHouseProduction
standard.EggWeight = detail.TargetEggWeight standard.EggWeight = detail.TargetEggWeight
standard.EggMesh = detail.TargetEggMass standard.EggMass = detail.TargetEggMass
} }
} }
@@ -1420,11 +1379,11 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
} }
} }
item.StandardHandDay = standard.HandDay item.StandardHenDay = standard.HenDay
item.StandardHandHouse = standard.HandHouse item.StandardHenHouse = standard.HenHouse
item.StandardFeedIntake = standard.FeedIntake item.StandardFeedIntake = standard.FeedIntake
item.StandardMaxDepletion = standard.MaxDepletion item.StandardMaxDepletion = standard.MaxDepletion
item.StandardEggMesh = standard.EggMesh item.StandardEggMass = standard.EggMass
item.StandardEggWeight = standard.EggWeight item.StandardEggWeight = standard.EggWeight
item.StandardFcr = standardFcr item.StandardFcr = standardFcr
@@ -346,17 +346,31 @@ func buildChartWeekSummary(weights []float64) utypes.UniformityChartWeek {
minBucket := bucket minBucket := bucket
maxBucket := bucket + bucketSize - 1 maxBucket := bucket + bucketSize - 1
count := 0.0 count := 0.0
bucketWeights := make([]float64, 0)
idealWeights := make([]float64, 0)
outsideWeights := make([]float64, 0)
for _, w := range weights { for _, w := range weights {
if w >= minBucket && w < minBucket+bucketSize { if w >= minBucket && w < minBucket+bucketSize {
count++ count++
bucketWeights = append(bucketWeights, w)
if w >= idealMin && w <= idealMax {
idealWeights = append(idealWeights, w)
} else {
outsideWeights = append(outsideWeights, w)
}
} }
} }
idealRangeLabel := rangeFromValues(idealWeights)
outsideRangeLabel := rangeFromValues(outsideWeights)
isIdealRange := idealRangeLabel != ""
distribution = append(distribution, utypes.UniformityChartRange{ distribution = append(distribution, utypes.UniformityChartRange{
Range: fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)), Range: fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)),
MinWeight: minBucket, MinWeight: minBucket,
MaxWeight: maxBucket, MaxWeight: maxBucket,
BirdCount: count, BirdCount: count,
IsIdealRange: minBucket >= idealMin && maxBucket <= idealMax, IsIdealRange: isIdealRange,
IdealRange: idealRangeLabel,
OutsideRange: outsideRangeLabel,
}) })
} }
@@ -391,3 +405,29 @@ func roundToPrecision(value float64, precision int) float64 {
} }
return math.Floor(scaled) / scale return math.Floor(scaled) / scale
} }
func rangeFromValues(values []float64) string {
if len(values) == 0 {
return ""
}
minValue := values[0]
maxValue := values[0]
for _, v := range values[1:] {
if v < minValue {
minValue = v
}
if v > maxValue {
maxValue = v
}
}
return formatRange(minValue, maxValue)
}
func formatRange(minValue, maxValue float64) string {
minInt := int(math.Round(minValue))
maxInt := int(math.Round(maxValue))
if minInt == maxInt {
return fmt.Sprintf("%d", minInt)
}
return fmt.Sprintf("%d-%d", minInt, maxInt)
}
@@ -199,14 +199,26 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa
for _, group := range grouped { for _, group := range grouped {
allWeeks := make(map[int]utypes.UniformityChartWeek) allWeeks := make(map[int]utypes.UniformityChartWeek)
weekOrder := make([]int, 0, len(group)) weekOrder := make([]int, 0, len(group))
weekSeen := make(map[int]struct{}, len(group))
weeksWithData := 0 weeksWithData := 0
gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group)) gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group))
latestByWeek := make(map[int]entity.ProjectFlockKandangUniformity)
for _, item := range group { for _, item := range group {
if item.Week == 0 { if item.Week == 0 {
continue continue
} }
if existing, ok := latestByWeek[item.Week]; !ok || isUniformityNewer(item, existing) {
latestByWeek[item.Week] = item
}
}
for week := range latestByWeek {
weekOrder = append(weekOrder, week)
}
sort.Ints(weekOrder)
for _, week := range weekOrder {
item := latestByWeek[week]
var weekSummary utypes.UniformityChartWeek var weekSummary utypes.UniformityChartWeek
if len(item.ChartData) > 0 { if len(item.ChartData) > 0 {
if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil { if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil {
@@ -222,16 +234,11 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa
if weekSummary.HasData { if weekSummary.HasData {
weeksWithData++ weeksWithData++
} }
allWeeks[item.Week] = weekSummary allWeeks[week] = weekSummary
if _, ok := weekSeen[item.Week]; !ok {
weekSeen[item.Week] = struct{}{}
weekOrder = append(weekOrder, item.Week)
}
hasData := item.ChickQtyOfWeight > 0 hasData := item.ChickQtyOfWeight > 0
gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{ gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{
Week: item.Week, Week: week,
UniformityPercent: item.Uniformity, UniformityPercent: item.Uniformity,
IdealCount: item.UniformQty, IdealCount: item.UniformQty,
OutsideIdealCount: item.NotUniformQty, OutsideIdealCount: item.NotUniformQty,
@@ -240,11 +247,6 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa
}) })
} }
sort.Ints(weekOrder)
sort.Slice(gaugeWeeks, func(i, j int) bool {
return gaugeWeeks[i].Week < gaugeWeeks[j].Week
})
weekIndex := make(map[int]int, len(weekOrder)) weekIndex := make(map[int]int, len(weekOrder))
for idx, week := range weekOrder { for idx, week := range weekOrder {
weekIndex[week] = idx weekIndex[week] = idx
@@ -280,6 +282,23 @@ func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKa
return result, nil return result, nil
} }
func isUniformityNewer(a, b entity.ProjectFlockKandangUniformity) bool {
var aDate, bDate time.Time
if a.UniformDate != nil {
aDate = *a.UniformDate
}
if b.UniformDate != nil {
bDate = *b.UniformDate
}
if !aDate.IsZero() || !bDate.IsZero() {
if aDate.Equal(bDate) {
return a.Id > b.Id
}
return aDate.After(bDate)
}
return a.Id > b.Id
}
func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) { func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
if s.DocumentSvc == nil || len(items) == 0 { if s.DocumentSvc == nil || len(items) == 0 {
return map[uint]string{}, nil return map[uint]string{}, nil
@@ -586,13 +605,13 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
} }
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error { func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
if projectFlockKandangID == 0 || week == 0 || uniformDate == nil || uniformDate.IsZero() { if projectFlockKandangID == 0 || week == 0 {
return nil return nil
} }
query := s.Repository.DB().WithContext(ctx). query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandangUniformity{}). Model(&entity.ProjectFlockKandangUniformity{}).
Where("project_flock_kandang_id = ? AND week = ? AND uniform_date = ?", projectFlockKandangID, week, *uniformDate) Where("project_flock_kandang_id = ? AND week = ?", projectFlockKandangID, week)
if id != 0 { if id != 0 {
query = query.Where("id <> ?", id) query = query.Where("id <> ?", id)
} }
@@ -602,7 +621,7 @@ func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint,
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness") return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness")
} }
if count > 0 { if count > 0 {
return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang, week, and date") return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang and week")
} }
return nil return nil
} }
@@ -29,6 +29,8 @@ type UniformityChartRange struct {
MaxWeight float64 `json:"max_weight"` MaxWeight float64 `json:"max_weight"`
BirdCount float64 `json:"bird_count"` BirdCount float64 `json:"bird_count"`
IsIdealRange bool `json:"is_ideal_range"` IsIdealRange bool `json:"is_ideal_range"`
IdealRange string `json:"ideal_range,omitempty"`
OutsideRange string `json:"outside_range,omitempty"`
} }
type UniformityChartIdealRange struct { type UniformityChartIdealRange struct {
@@ -82,4 +84,4 @@ type UniformityChartGauge struct {
type UniformityChartData struct { type UniformityChartData struct {
BarChart UniformityChartBar `json:"bar_chart"` BarChart UniformityChartBar `json:"bar_chart"`
GaugeChart UniformityChartGauge `json:"gauge_chart"` GaugeChart UniformityChartGauge `json:"gauge_chart"`
} }
@@ -25,6 +25,7 @@ type PurchaseRepository interface {
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 BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
} }
@@ -89,6 +90,44 @@ WHERE pi.purchase_id = ?
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
} }
func (r *PurchaseRepositoryImpl) SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
if len(projectFlockKandangIDs) == 0 {
return nil
}
return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var purchaseIDs []uint
query := `
SELECT pi.purchase_id
FROM purchase_items pi
WHERE pi.project_flock_kandang_id IN (?)
GROUP BY pi.purchase_id
HAVING COUNT(*) = COUNT(CASE WHEN pi.project_flock_kandang_id IN (?) THEN 1 END)
`
if err := tx.Raw(query, projectFlockKandangIDs, projectFlockKandangIDs).Scan(&purchaseIDs).Error; err != nil {
return err
}
now := time.Now().UTC()
if len(purchaseIDs) > 0 {
if err := tx.Model(&entity.Purchase{}).
Where("id IN (?) AND deleted_at IS NULL", purchaseIDs).
Update("deleted_at", now).Error; err != nil {
return err
}
if err := tx.Where("purchase_id IN (?)", purchaseIDs).Delete(&entity.PurchaseItem{}).Error; err != nil {
return err
}
}
deleteItems := tx.Where("project_flock_kandang_id IN (?)", projectFlockKandangIDs)
if len(purchaseIDs) > 0 {
deleteItems = deleteItems.Where("purchase_id NOT IN (?)", purchaseIDs)
}
return deleteItems.Delete(&entity.PurchaseItem{}).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
@@ -594,7 +594,7 @@ func (b *expenseBridge) createExpenseViaService(
req := &expenseValidation.Create{ req := &expenseValidation.Create{
PoNumber: "", PoNumber: "",
TransactionDate: utils.FormatDate(expenseDate), TransactionDate: utils.FormatDate(expenseDate),
Category: "BOP", Category: string(utils.ExpenseCategoryBOP),
SupplierID: uint64(supplierID), SupplierID: uint64(supplierID),
LocationID: locationID, LocationID: locationID,
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{ ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
@@ -133,6 +133,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
db = db.Where("purchases.deleted_at IS NULL")
if params.SupplierID > 0 { if params.SupplierID > 0 {
db = db.Where("supplier_id = ?", params.SupplierID) db = db.Where("supplier_id = ?", params.SupplierID)
@@ -107,21 +107,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String() transferStockableKey := fifo.StockableKeyStockTransferIn.String()
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select(` Select(`
k.id AS kandang_id, k.id AS kandang_id,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END), 0) AS feed_cost, END), 0) AS feed_cost,
COALESCE(SUM(CASE COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END), 0) AS ovk_cost`, END), 0) AS ovk_cost`,
utils.FlagPakan, transferStockableKey, utils.FlagPakan, utils.FlagPakan, transferStockableKey, utils.FlagPakan,
utils.FlagOVK, transferStockableKey, utils.FlagOVK). utils.FlagOVK, transferStockableKey, utils.FlagOVK).
+38
View File
@@ -2,11 +2,14 @@ package utils
import ( import (
"errors" "errors"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/validation" "gitlab.com/mbugroup/lti-api.git/internal/common/validation"
"gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jackc/pgconn"
pgconnv5 "github.com/jackc/pgx/v5/pgconn"
) )
func ErrorHandler(c *fiber.Ctx, err error) error { func ErrorHandler(c *fiber.Ctx, err error) error {
@@ -14,6 +17,10 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
return response.Error(c, fiber.StatusBadRequest, message, nil) return response.Error(c, fiber.StatusBadRequest, message, nil)
} }
if statusCode, message := mapPgError(err); statusCode != 0 {
return response.Error(c, statusCode, message, nil)
}
var fiberErr *fiber.Error var fiberErr *fiber.Error
if errors.As(err, &fiberErr) { if errors.As(err, &fiberErr) {
return response.Error(c, fiberErr.Code, fiberErr.Message, nil) return response.Error(c, fiberErr.Code, fiberErr.Message, nil)
@@ -26,6 +33,37 @@ func NotFoundHandler(c *fiber.Ctx) error {
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil) return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
} }
func mapPgError(err error) (int, string) {
code, message := getPgErrorDetails(err)
if code == "" {
return 0, ""
}
switch code {
case "23503":
return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain."
case "P0001":
if strings.HasPrefix(message, "Cannot soft delete") {
return fiber.StatusConflict, "Data tidak bisa dihapus karena masih digunakan oleh data lain."
}
}
return 0, ""
}
func getPgErrorDetails(err error) (string, string) {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code, pgErr.Message
}
var pgErrV5 *pgconnv5.PgError
if errors.As(err, &pgErrV5) {
return pgErrV5.Code, pgErrV5.Message
}
return "", ""
}
func BadRequest(msg string) error { func BadRequest(msg string) error {
return fiber.NewError(fiber.StatusBadRequest, msg) return fiber.NewError(fiber.StatusBadRequest, msg)