mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 14:55:42 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-390-Dashboard
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
|
DROP SEQUENCE IF EXISTS expenses_ref_seq;
|
||||||
DROP TABLE IF EXISTS expenses;
|
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;
|
||||||
|
|||||||
+4
@@ -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;
|
||||||
+10
@@ -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);
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE payments
|
||||||
|
DROP COLUMN IF EXISTS party_account_number;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+6
@@ -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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,7 +107,7 @@ 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").
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user