mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 21:41:55 +00:00
Merge branch 'dev/teguh' into 'development'
FEAT[BE]: add expense to transfer stock, make document optional on transfer stock, fixing closing penjualan bad request See merge request mbugroup/lti-api!144
This commit is contained in:
@@ -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_tasks;
|
||||
DROP TABLE IF EXISTS daily_checklist_tasks;
|
||||
DROP TABLE IF EXISTS daily_checklist_phases;
|
||||
DROP TABLE IF EXISTS daily_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);
|
||||
@@ -5,15 +5,15 @@ import (
|
||||
)
|
||||
|
||||
type ExpenseNonstock struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
ExpenseId *uint64 `gorm:""`
|
||||
ProjectFlockKandangId *uint64 `gorm:""`
|
||||
KandangId *uint64 `gorm:""`
|
||||
NonstockId *uint64 `gorm:""`
|
||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||
Notes string `gorm:"type:text;column:notes"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
ExpenseId *uint64 `gorm:""`
|
||||
ProjectFlockKandangId *uint64 `gorm:""`
|
||||
KandangId *uint64 `gorm:""`
|
||||
NonstockId *uint64 `gorm:""`
|
||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||
Notes string `gorm:"type:text;column:notes"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||
|
||||
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
|
||||
@@ -8,27 +8,22 @@ type StockTransferDetail struct {
|
||||
StockTransferId uint64
|
||||
ProductId uint64
|
||||
|
||||
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
|
||||
// Tracking stock yang DIAMBIL dari source warehouse
|
||||
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
||||
|
||||
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) ===
|
||||
// Tracking stock yang DITAMBAHKAN ke destination warehouse
|
||||
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
||||
|
||||
// === METADATA ===
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
||||
ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
|
||||
// === RELATIONS ===
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Product *Product `gorm:"foreignKey:ProductId"`
|
||||
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||
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 {
|
||||
param := c.Params("project_flock_id")
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
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)
|
||||
|
||||
return SalesDTO{
|
||||
Id: e.Id,
|
||||
RealizationDate: *e.DeliveryDate,
|
||||
RealizationDate: realizationDate,
|
||||
Age: age,
|
||||
DoNumber: doNumber,
|
||||
Product: product,
|
||||
Customer: customer,
|
||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
||||
Qty: e.UsageQty,
|
||||
Weight: e.TotalWeight,
|
||||
AvgWeight: e.AvgWeight,
|
||||
Price: e.UnitPrice,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
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/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -914,9 +915,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
||||
|
||||
var rows []ActualUsageCostRow
|
||||
|
||||
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll)
|
||||
purchaseStockableKey := "PURCHASE_ITEMS"
|
||||
transferStockableKey := "STOCK_TRANSFER_DETAILS"
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||
|
||||
recordingQuery := db.
|
||||
Table("recordings AS r").
|
||||
@@ -982,7 +982,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Part 2: Get usage from project_chickins (DOC, Pullet)
|
||||
chickinQuery := db.
|
||||
Table("project_chickins AS pc").
|
||||
Select(`
|
||||
@@ -1006,7 +1005,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge results
|
||||
rows = append(rows, chickinRows...)
|
||||
|
||||
return rows, nil
|
||||
|
||||
@@ -151,9 +151,19 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
||||
return nil, err
|
||||
}
|
||||
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) {
|
||||
@@ -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) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}},
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
|
||||
); err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
// Get actual usage cost instead of purchase items
|
||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
|
||||
@@ -75,6 +75,8 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
|
||||
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||
data := c.FormValue("data")
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024
|
||||
|
||||
var req validation.TransferRequest
|
||||
if err := json.Unmarshal([]byte(data), &req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
@@ -87,9 +89,11 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||
|
||||
files := form.File["documents"]
|
||||
|
||||
if len(files) != len(req.Deliveries) {
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message)
|
||||
for i, file := range files {
|
||||
if file.Size > maxFileSize {
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
"Dokumen ke-"+strconv.Itoa(i+1)+" melebihi ukuran maksimal 5MB")
|
||||
}
|
||||
}
|
||||
|
||||
result, err := u.TransferService.CreateOne(c, &req, files)
|
||||
|
||||
@@ -71,9 +71,11 @@ type TransferDetailDTO struct {
|
||||
}
|
||||
|
||||
type TransferDetailItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Product ProductSimpleDTO `json:"product"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Id uint64 `json:"id"`
|
||||
Product ProductSimpleDTO `json:"product"`
|
||||
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 {
|
||||
@@ -153,14 +155,30 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
detailDTO := TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
Product: ProductSimpleDTO{
|
||||
Id: d.Product.Id,
|
||||
Name: d.Product.Name,
|
||||
},
|
||||
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
|
||||
@@ -223,14 +241,30 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
detailDTO := TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
Product: ProductSimpleDTO{
|
||||
Id: d.Product.Id,
|
||||
Name: d.Product.Name,
|
||||
},
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@ package transfers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -9,9 +10,13 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
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"
|
||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||
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"
|
||||
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"
|
||||
@@ -35,15 +40,44 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||
|
||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||
if err != nil {
|
||||
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)
|
||||
expenseBridge := sTransfer.NewTransferExpenseBridge(
|
||||
db,
|
||||
stockTransferRepo,
|
||||
projectFlockKandangRepo,
|
||||
kandangRepo,
|
||||
expenseServiceInstance,
|
||||
)
|
||||
|
||||
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyStockTransferIn,
|
||||
@@ -77,7 +111,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
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)
|
||||
|
||||
TransferRoutes(router, userService, transferService)
|
||||
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -12,6 +13,7 @@ type StockTransferRepository interface {
|
||||
repository.BaseRepository[entity.StockTransfer]
|
||||
// get sequence for movement number
|
||||
GetNextMovementNumber(ctx context.Context) (int64, error)
|
||||
GenerateMovementNumber(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type StockTransferRepositoryImpl struct {
|
||||
@@ -32,3 +34,12 @@ func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context)
|
||||
}
|
||||
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
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
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{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -63,6 +64,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
DocumentSvc: documentSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
ExpenseBridge: expenseBridge,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +79,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ToWarehouse.Area").
|
||||
Preload("Details").
|
||||
Preload("Details.Product").
|
||||
Preload("Details.ExpenseNonstock").
|
||||
Preload("Details.ExpenseNonstock.Expense").
|
||||
Preload("Details.ExpenseNonstock.Expense.Supplier").
|
||||
Preload("Deliveries.Items").
|
||||
Preload("Deliveries.Supplier").
|
||||
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) {
|
||||
|
||||
// === VALIDASI SOURCE WAREHOUSE ===
|
||||
pwIDs := make([]uint, 0, len(req.Products))
|
||||
|
||||
for _, product := range req.Products {
|
||||
@@ -155,14 +159,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.ProjectFlockKandangRepo != nil {
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||
}
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
}
|
||||
if projectFlockKandang.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||
}
|
||||
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
|
||||
|
||||
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
||||
|
||||
entityTransfer := &entity.StockTransfer{
|
||||
@@ -213,19 +215,26 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
CreatedBy: uint64(actorID),
|
||||
}
|
||||
|
||||
expensePayloads := make([]TransferExpenseReceivingPayload, 0)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Prepare details and fetch product warehouses
|
||||
details := make([]*entity.StockTransferDetail, 0, len(req.Products))
|
||||
detailMap := make(map[uint64]*entity.StockTransferDetail)
|
||||
|
||||
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),
|
||||
)
|
||||
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")
|
||||
}
|
||||
|
||||
// Get or create destination product warehouse
|
||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -254,7 +262,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
Quantity: 0,
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -275,7 +283,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
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
|
||||
}
|
||||
|
||||
@@ -290,7 +298,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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{
|
||||
{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeTransfer),
|
||||
Index: &idx,
|
||||
Index: &reqDelivery.DocumentIndex,
|
||||
},
|
||||
}
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeTransfer),
|
||||
DocumentableID: deliveries[idx].Id,
|
||||
DocumentableID: delivery.Id,
|
||||
CreatedBy: &actorID,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
|
||||
idx+1, deliveries[idx].Id, file.Filename)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err))
|
||||
deliveryIdx+1, delivery.Id, file.Filename)
|
||||
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)
|
||||
}
|
||||
|
||||
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
|
||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
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
|
||||
})
|
||||
|
||||
@@ -396,9 +443,31 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, 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 {
|
||||
DeliveryCost float64 `json:"delivery_cost" 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"`
|
||||
VehiclePlate string `json:"vehicle_plate" validate:"required"`
|
||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||
|
||||
@@ -20,6 +20,7 @@ type ProjectflockRepository interface {
|
||||
GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error)
|
||||
GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, 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)
|
||||
FcrExists(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) {
|
||||
return repository.Exists[entity.Area](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@ func (b *expenseBridge) createExpenseViaService(
|
||||
req := &expenseValidation.Create{
|
||||
PoNumber: "",
|
||||
TransactionDate: utils.FormatDate(expenseDate),
|
||||
Category: "BOP",
|
||||
Category: string(utils.ExpenseCategoryBOP),
|
||||
SupplierID: uint64(supplierID),
|
||||
LocationID: locationID,
|
||||
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
||||
|
||||
@@ -107,21 +107,21 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
|
||||
transferStockableKey := fifo.StockableKeyStockTransferIn.String()
|
||||
|
||||
query := r.db.WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
k.id AS kandang_id,
|
||||
COALESCE(SUM(CASE
|
||||
COALESCE(SUM(CASE
|
||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
ELSE 0
|
||||
END), 0) AS feed_cost,
|
||||
COALESCE(SUM(CASE
|
||||
COALESCE(SUM(CASE
|
||||
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
ELSE 0
|
||||
END), 0) AS ovk_cost`,
|
||||
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
|
||||
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
|
||||
|
||||
Reference in New Issue
Block a user