mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dc4592f08 | |||
| 690db8b485 | |||
| 22bf66dbb9 | |||
| f836685253 | |||
| 5e9286428f | |||
| 61e15dd95d | |||
| 59d72f20b4 | |||
| 540434e33b | |||
| 0ebad48348 | |||
| 0a900986e7 | |||
| b3887b8d08 | |||
| 2ddfa57aed | |||
| 085d2f9bfe |
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- Reverse UPSERT: hapus baris PFK 47 & 48 yang kemungkinan baru diinsert oleh up migration ini.
|
||||||
|
-- Jika sebelumnya sudah ada (ON CONFLICT DO UPDATE), baris ini akan terhapus —
|
||||||
|
-- restore manual dari backup jika diperlukan.
|
||||||
|
DELETE FROM farm_depreciation_manual_inputs
|
||||||
|
WHERE project_flock_id IN (47, 48);
|
||||||
|
|
||||||
|
-- UPDATE rows untuk PFK 4–27 tidak bisa di-reverse secara presisi:
|
||||||
|
-- nilai total_cost sebelum migration ini tidak tersimpan di migration history
|
||||||
|
-- (data awal di-load via cmd/import-farm-depreciation-manual-inputs dari Excel).
|
||||||
|
-- PFK 10 dan 11 tidak berubah (nilai sama dengan state dari migration 20260529144559).
|
||||||
|
-- Jika perlu rollback penuh: restore dari database backup atau re-import Excel lama.
|
||||||
|
|
||||||
|
-- Recompute snapshots setelah rollback
|
||||||
|
TRUNCATE TABLE farm_depreciation_snapshots;
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 1900157533.55,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 10;
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 146658321.066,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 13;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 51824694.138,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 17;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 15491774.796,
|
||||||
|
cutover_date = DATE '2026-02-28',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 8;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Cutover 2026-02-28 (lanjutan)
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 575074391.36, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 4;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 578360642.51, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 5;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 880983605.92, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 6;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 391669576.153, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 9;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 2521797832.14, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 11;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 139227054.164, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 12;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 380083106.836, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 14;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 705136853.847, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 15;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 209816474.000, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 18;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 557606867.000, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 19;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 239330456.11, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 20;
|
||||||
|
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 4724203916.72, cutover_date = DATE '2026-02-28', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 26;
|
||||||
|
|
||||||
|
-- Cutover 2026-05-15
|
||||||
|
UPDATE farm_depreciation_manual_inputs
|
||||||
|
SET total_cost = 5449963647.43, cutover_date = DATE '2026-05-15', updated_at = NOW()
|
||||||
|
WHERE project_flock_id = 27;
|
||||||
|
|
||||||
|
-- Cutover 2026-06-08 (upsert — row mungkin belum ada)
|
||||||
|
INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at)
|
||||||
|
VALUES (47, 5395429899.42, DATE '2026-06-08', NOW(), NOW())
|
||||||
|
ON CONFLICT (project_flock_id) DO UPDATE
|
||||||
|
SET total_cost = EXCLUDED.total_cost,
|
||||||
|
cutover_date = EXCLUDED.cutover_date,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- Cutover 2026-06-16 (upsert — row mungkin belum ada)
|
||||||
|
INSERT INTO farm_depreciation_manual_inputs (project_flock_id, total_cost, cutover_date, created_at, updated_at)
|
||||||
|
VALUES (48, 5514616442.08, DATE '2026-06-16', NOW(), NOW())
|
||||||
|
ON CONFLICT (project_flock_id) DO UPDATE
|
||||||
|
SET total_cost = EXCLUDED.total_cost,
|
||||||
|
cutover_date = EXCLUDED.cutover_date,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- Pengaman: pastikan snapshot di-recompute dengan total_cost baru
|
||||||
|
-- saat user request /api/reports/expense/depreciation
|
||||||
|
TRUNCATE TABLE farm_depreciation_snapshots;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE phases SET is_active = true
|
||||||
|
WHERE id IN (2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE phases SET is_active = false
|
||||||
|
WHERE id IN (2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26);
|
||||||
@@ -7,7 +7,20 @@ type RecordingStock struct {
|
|||||||
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id;index"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id;index"`
|
||||||
UsageQty *float64 `gorm:"column:usage_qty"`
|
UsageQty *float64 `gorm:"column:usage_qty"`
|
||||||
PendingQty *float64 `gorm:"column:pending_qty"`
|
PendingQty *float64 `gorm:"column:pending_qty"`
|
||||||
|
TotalPrice float64 `gorm:"-"`
|
||||||
|
Allocations []RecordingStockAlloc `gorm:"-"`
|
||||||
|
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordingStockAlloc struct {
|
||||||
|
SourceType string
|
||||||
|
SourceId uint
|
||||||
|
PrNumber string
|
||||||
|
PoNumber string
|
||||||
|
AdjNumber string
|
||||||
|
Qty float64
|
||||||
|
UnitPrice float64
|
||||||
|
Subtotal float64
|
||||||
|
}
|
||||||
|
|||||||
@@ -1215,7 +1215,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(phaseIDs) > 0 {
|
if len(phaseIDs) > 0 {
|
||||||
phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, nil)
|
phases, err := s.PhaseRepo.GetByIDs(c.Context(), phaseIDs, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("is_active = true")
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Phase not found")
|
return fiber.NewError(fiber.StatusBadRequest, "Phase not found")
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ func (s kandangGroupService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if params.OrderBy == "desc" || params.OrderBy == "" {
|
if params.OrderBy == "desc" || params.OrderBy == "" {
|
||||||
db = db.Order(fmt.Sprintf("kandang_groups.%s DESC", params.SortBy))
|
db = db.Order(fmt.Sprintf("kandang_groups.%s DESC, kandang_groups.id ASC", params.SortBy))
|
||||||
} else {
|
} else {
|
||||||
db = db.Order(fmt.Sprintf("kandang_groups.%s ASC", params.SortBy))
|
db = db.Order(fmt.Sprintf("kandang_groups.%s ASC, kandang_groups.id ASC", params.SortBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ type Query struct {
|
|||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"`
|
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"name"`
|
||||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"`
|
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"asc"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.
|
|||||||
|
|
||||||
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
db = db.Where("is_active = true")
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,10 +131,23 @@ type RecordingDepletionDTO struct {
|
|||||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordingStockAllocDTO struct {
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
SourceId uint `json:"source_id"`
|
||||||
|
PrNumber string `json:"pr_number"`
|
||||||
|
PoNumber string `json:"po_number"`
|
||||||
|
AdjNumber string `json:"adj_number"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
Subtotal float64 `json:"subtotal"`
|
||||||
|
}
|
||||||
|
|
||||||
type RecordingStockDTO struct {
|
type RecordingStockDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
UsageAmount float64 `json:"usage_amount"`
|
UsageAmount float64 `json:"usage_amount"`
|
||||||
PendingQty float64 `json:"pending_qty"`
|
PendingQty float64 `json:"pending_qty"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Allocations []RecordingStockAllocDTO `json:"allocations"`
|
||||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +210,26 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
|||||||
pendingQty = *s.PendingQty
|
pendingQty = *s.PendingQty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allocs := make([]RecordingStockAllocDTO, len(s.Allocations))
|
||||||
|
for j, a := range s.Allocations {
|
||||||
|
allocs[j] = RecordingStockAllocDTO{
|
||||||
|
SourceType: a.SourceType,
|
||||||
|
SourceId: a.SourceId,
|
||||||
|
PrNumber: a.PrNumber,
|
||||||
|
PoNumber: a.PoNumber,
|
||||||
|
AdjNumber: a.AdjNumber,
|
||||||
|
Qty: a.Qty,
|
||||||
|
UnitPrice: a.UnitPrice,
|
||||||
|
Subtotal: a.Subtotal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result[i] = RecordingStockDTO{
|
result[i] = RecordingStockDTO{
|
||||||
ProductWarehouseId: s.ProductWarehouseId,
|
ProductWarehouseId: s.ProductWarehouseId,
|
||||||
UsageAmount: usageAmount,
|
UsageAmount: usageAmount,
|
||||||
PendingQty: pendingQty,
|
PendingQty: pendingQty,
|
||||||
|
TotalPrice: s.TotalPrice,
|
||||||
|
Allocations: allocs,
|
||||||
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ type RecordingRepository interface {
|
|||||||
ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error
|
ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error
|
||||||
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
|
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
|
||||||
GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error)
|
GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error)
|
||||||
|
GetStockAllocationsByIDs(ctx context.Context, stockIDs []uint) (map[uint][]entity.RecordingStockAlloc, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingRepositoryImpl struct {
|
type RecordingRepositoryImpl struct {
|
||||||
@@ -1231,3 +1232,71 @@ func (r *RecordingRepositoryImpl) GetTotalWeightProducedFromUniformityByProjectF
|
|||||||
|
|
||||||
return result.TotalWeight, err
|
return result.TotalWeight, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetStockAllocationsByIDs(ctx context.Context, stockIDs []uint) (map[uint][]entity.RecordingStockAlloc, error) {
|
||||||
|
if len(stockIDs) == 0 {
|
||||||
|
return map[uint][]entity.RecordingStockAlloc{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
RecordingStockId uint
|
||||||
|
SourceType string
|
||||||
|
SourceId uint
|
||||||
|
PrNumber string
|
||||||
|
PoNumber string
|
||||||
|
AdjNumber string
|
||||||
|
Qty float64
|
||||||
|
UnitPrice float64
|
||||||
|
Subtotal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []row
|
||||||
|
err := r.DB().WithContext(ctx).Raw(`
|
||||||
|
SELECT
|
||||||
|
sa.usable_id AS recording_stock_id,
|
||||||
|
sa.stockable_type AS source_type,
|
||||||
|
sa.stockable_id AS source_id,
|
||||||
|
COALESCE(p.pr_number, '') AS pr_number,
|
||||||
|
COALESCE(p.po_number, '') AS po_number,
|
||||||
|
COALESCE(ast.adj_number, '') AS adj_number,
|
||||||
|
sa.qty AS qty,
|
||||||
|
COALESCE(CASE
|
||||||
|
WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN pi.price
|
||||||
|
WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN ast.price
|
||||||
|
END, 0) AS unit_price,
|
||||||
|
sa.qty * COALESCE(CASE
|
||||||
|
WHEN sa.stockable_type = 'PURCHASE_ITEMS' THEN pi.price
|
||||||
|
WHEN sa.stockable_type = 'ADJUSTMENT_IN' THEN ast.price
|
||||||
|
END, 0) AS subtotal
|
||||||
|
FROM stock_allocations sa
|
||||||
|
LEFT JOIN purchase_items pi
|
||||||
|
ON pi.id = sa.stockable_id AND sa.stockable_type = 'PURCHASE_ITEMS'
|
||||||
|
LEFT JOIN purchases p
|
||||||
|
ON p.id = pi.purchase_id
|
||||||
|
LEFT JOIN adjustment_stocks ast
|
||||||
|
ON ast.id = sa.stockable_id AND sa.stockable_type = 'ADJUSTMENT_IN'
|
||||||
|
WHERE sa.usable_type = 'RECORDING_STOCK'
|
||||||
|
AND sa.usable_id IN ?
|
||||||
|
AND sa.status = 'ACTIVE'
|
||||||
|
AND sa.allocation_purpose = 'CONSUME'
|
||||||
|
ORDER BY sa.usable_id, sa.id
|
||||||
|
`, stockIDs).Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint][]entity.RecordingStockAlloc)
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.RecordingStockId] = append(result[row.RecordingStockId], entity.RecordingStockAlloc{
|
||||||
|
SourceType: row.SourceType,
|
||||||
|
SourceId: row.SourceId,
|
||||||
|
PrNumber: row.PrNumber,
|
||||||
|
PoNumber: row.PoNumber,
|
||||||
|
AdjNumber: row.AdjNumber,
|
||||||
|
Qty: row.Qty,
|
||||||
|
UnitPrice: row.UnitPrice,
|
||||||
|
Subtotal: row.Subtotal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordingService interface {
|
type RecordingService interface {
|
||||||
@@ -279,6 +278,26 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
|||||||
s.Log.Errorf("Failed get recording by id: %+v", err)
|
s.Log.Errorf("Failed get recording by id: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(recording.Stocks) > 0 {
|
||||||
|
stockIDs := make([]uint, len(recording.Stocks))
|
||||||
|
for i, s := range recording.Stocks {
|
||||||
|
stockIDs[i] = s.Id
|
||||||
|
}
|
||||||
|
if allocMap, err := s.Repository.GetStockAllocationsByIDs(c.Context(), stockIDs); err != nil {
|
||||||
|
s.Log.Warnf("Failed to get stock allocations for recording %d: %+v", id, err)
|
||||||
|
} else {
|
||||||
|
for i := range recording.Stocks {
|
||||||
|
allocs := allocMap[recording.Stocks[i].Id]
|
||||||
|
recording.Stocks[i].Allocations = allocs
|
||||||
|
var total float64
|
||||||
|
for _, a := range allocs {
|
||||||
|
total += a.Subtotal
|
||||||
|
}
|
||||||
|
recording.Stocks[i].TotalPrice = total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := recordingutil.AttachLatestApproval(c.Context(), recording, s.ApprovalSvc, s.Log); err != nil {
|
if err := recordingutil.AttachLatestApproval(c.Context(), recording, s.ApprovalSvc, s.Log); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -586,10 +605,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
s.Log.Errorf("Failed to recalculate recordings after create: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
action := entity.ApprovalActionCreated
|
action := entity.ApprovalActionCreated
|
||||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
||||||
@@ -892,12 +907,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasStockChanges {
|
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action := entity.ApprovalActionUpdated
|
action := entity.ApprovalActionUpdated
|
||||||
actorID := recordingEntity.CreatedBy
|
actorID := recordingEntity.CreatedBy
|
||||||
@@ -1159,10 +1168,6 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil {
|
|
||||||
s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1949,172 +1954,6 @@ func (s *recordingService) getEarliestChickInDateByProjectFlockKandangID(ctx con
|
|||||||
return row.ChickInDate, nil
|
return row.ChickInDate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) syncFarmDepreciationManualInputFromRecordingStocks(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
projectFlockKandangID uint,
|
|
||||||
fallbackCutoverDate time.Time,
|
|
||||||
) error {
|
|
||||||
if projectFlockKandangID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targetDB := s.Repository.DB()
|
|
||||||
if tx != nil {
|
|
||||||
targetDB = tx
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlockID, err := s.resolveProjectFlockIDByProjectFlockKandangID(ctx, targetDB, projectFlockKandangID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalCost, err := s.sumNoTransferRecordingStockCostByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := s.getFarmDepreciationManualInputByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cutoverDate := normalizeDateOnlyUTC(fallbackCutoverDate)
|
|
||||||
if existing != nil && !existing.CutoverDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(existing.CutoverDate)
|
|
||||||
}
|
|
||||||
if cutoverDate.IsZero() {
|
|
||||||
earliestDate, dateErr := s.getEarliestNoTransferRecordingDateByProjectFlockID(ctx, targetDB, projectFlockID)
|
|
||||||
if dateErr != nil {
|
|
||||||
return dateErr
|
|
||||||
}
|
|
||||||
if earliestDate != nil && !earliestDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(*earliestDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cutoverDate.IsZero() {
|
|
||||||
cutoverDate = normalizeDateOnlyUTC(time.Now().UTC())
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
|
||||||
row := entity.FarmDepreciationManualInput{
|
|
||||||
ProjectFlockId: projectFlockID,
|
|
||||||
TotalCost: totalCost,
|
|
||||||
CutoverDate: cutoverDate,
|
|
||||||
}
|
|
||||||
if existing != nil {
|
|
||||||
row.Note = existing.Note
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetDB.WithContext(ctx).
|
|
||||||
Clauses(clause.OnConflict{
|
|
||||||
Columns: []clause.Column{{Name: "project_flock_id"}},
|
|
||||||
DoUpdates: clause.Assignments(map[string]any{
|
|
||||||
"total_cost": row.TotalCost,
|
|
||||||
"cutover_date": row.CutoverDate,
|
|
||||||
"updated_at": now,
|
|
||||||
}),
|
|
||||||
}).
|
|
||||||
Create(&row).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) resolveProjectFlockIDByProjectFlockKandangID(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (uint, error) {
|
|
||||||
var row struct {
|
|
||||||
ProjectFlockID uint `gorm:"column:project_flock_id"`
|
|
||||||
}
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("project_flock_kandangs").
|
|
||||||
Select("project_flock_id").
|
|
||||||
Where("id = ?", projectFlockKandangID).
|
|
||||||
Take(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return row.ProjectFlockID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) sumNoTransferRecordingStockCostByProjectFlockID(ctx context.Context, db *gorm.DB, projectFlockID uint) (float64, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var total float64
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("recording_stocks AS rs").
|
|
||||||
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)").
|
|
||||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Joins(
|
|
||||||
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
||||||
fifo.UsableKeyRecordingStock.String(),
|
|
||||||
fifo.StockableKeyPurchaseItems.String(),
|
|
||||||
entity.StockAllocationStatusActive,
|
|
||||||
entity.StockAllocationPurposeConsume,
|
|
||||||
).
|
|
||||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("rs.project_flock_kandang_id IS NULL").
|
|
||||||
Scan(&total).Error
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getFarmDepreciationManualInputByProjectFlockID(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
projectFlockID uint,
|
|
||||||
) (*entity.FarmDepreciationManualInput, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var row entity.FarmDepreciationManualInput
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
|
||||||
Take(&row).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &row, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getEarliestNoTransferRecordingDateByProjectFlockID(
|
|
||||||
ctx context.Context,
|
|
||||||
db *gorm.DB,
|
|
||||||
projectFlockID uint,
|
|
||||||
) (*time.Time, error) {
|
|
||||||
if projectFlockID == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var row struct {
|
|
||||||
RecordDate *time.Time `gorm:"column:record_date"`
|
|
||||||
}
|
|
||||||
err := db.WithContext(ctx).
|
|
||||||
Table("recording_stocks AS rs").
|
|
||||||
Select("MIN(r.record_datetime) AS record_date").
|
|
||||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
|
||||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
|
||||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("rs.project_flock_kandang_id IS NULL").
|
|
||||||
Scan(&row).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return row.RecordDate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
func (s *recordingService) resolveEggRequestsToFarmWarehouses(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pfk *entity.ProjectFlockKandang,
|
pfk *entity.ProjectFlockKandang,
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToMarketingReportItemsUsesDeliveryProductTotalWeight(t *testing.T) {
|
||||||
|
mdps := []entity.MarketingDeliveryProduct{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
UsageQty: 10,
|
||||||
|
AvgWeight: 2.5,
|
||||||
|
TotalWeight: 17.75,
|
||||||
|
UnitPrice: 1000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ToMarketingReportItems(mdps, nil, nil, nil)
|
||||||
|
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 marketing report item, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0].TotalWeightKg != 17.75 {
|
||||||
|
t.Fatalf("expected total_weight_kg to use delivery product total_weight 17.75, got %.2f", got[0].TotalWeightKg)
|
||||||
|
}
|
||||||
|
if got[0].Qty != 10 {
|
||||||
|
t.Fatalf("expected qty to stay from usage_qty, got %.2f", got[0].Qty)
|
||||||
|
}
|
||||||
|
if got[0].AverageWeightKg != 2.5 {
|
||||||
|
t.Fatalf("expected average_weight_kg to stay from avg_weight, got %.2f", got[0].AverageWeightKg)
|
||||||
|
}
|
||||||
|
if got[0].SalesAmount != 17750 {
|
||||||
|
t.Fatalf("expected sales_amount to use delivery product total_weight, got %.2f", got[0].SalesAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarketingSummaryUsesReportItemTotalWeight(t *testing.T) {
|
||||||
|
items := []RepportMarketingItemDTO{
|
||||||
|
{
|
||||||
|
Qty: 10,
|
||||||
|
TotalWeightKg: 17.75,
|
||||||
|
SalesAmount: 17750,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Qty: 5,
|
||||||
|
TotalWeightKg: 8.25,
|
||||||
|
SalesAmount: 8250,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ToSummaryFromDTOItems(items)
|
||||||
|
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("expected summary, got nil")
|
||||||
|
}
|
||||||
|
if got.TotalWeightKg != 26 {
|
||||||
|
t.Fatalf("expected summary total_weight_kg to sum item total weights, got %.2f", got.TotalWeightKg)
|
||||||
|
}
|
||||||
|
if diff := math.Abs(got.AverageWeightKg - (26.0 / 15.0)); diff > 0.000001 {
|
||||||
|
t.Fatalf("expected summary average_weight_kg to use total_weight_kg / total_qty, got %.6f", got.AverageWeightKg)
|
||||||
|
}
|
||||||
|
if got.TotalQty != 15 {
|
||||||
|
t.Fatalf("expected total qty 15, got %d", got.TotalQty)
|
||||||
|
}
|
||||||
|
if got.TotalSalesAmount != 26000 {
|
||||||
|
t.Fatalf("expected total sales amount 26000, got %d", got.TotalSalesAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user