init depresiasi

This commit is contained in:
giovanni
2026-04-17 21:26:56 +07:00
parent a54c6184a2
commit fcde3b0a36
34 changed files with 3588 additions and 46 deletions
+212
View File
@@ -8559,6 +8559,218 @@
] ]
} }
}, },
"/api/reports/expense/depreciation": {
"get": {
"description": "Read access to `/api/reports/expense/depreciation`.",
"parameters": [
{
"description": "Page number.",
"example": 1,
"in": "query",
"name": "page",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "Page size.",
"example": 10,
"in": "query",
"name": "limit",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "Daily period filter (YYYY-MM-DD).",
"example": "2026-01-01",
"in": "query",
"name": "period",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Comma separated project flock ids.",
"example": "1,2",
"in": "query",
"name": "project_flock_id",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Comma separated area ids.",
"example": "1,2",
"in": "query",
"name": "area_id",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Comma separated location ids.",
"example": "1,2",
"in": "query",
"name": "location_id",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedEnvelope"
}
}
},
"description": "Successful response"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"description": "Forbidden"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"BearerAuth": []
}
],
"summary": "GET api / reports / expense / depreciation",
"tags": [
"Reports"
]
}
},
"/api/reports/expense/depreciation/manual-inputs": {
"get": {
"description": "Read access to `/api/reports/expense/depreciation/manual-inputs`.",
"parameters": [
{
"description": "Page number.",
"example": 1,
"in": "query",
"name": "page",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "Page size.",
"example": 10,
"in": "query",
"name": "limit",
"required": false,
"schema": {
"type": "integer"
}
},
{
"description": "Comma separated project flock ids.",
"example": "1,2",
"in": "query",
"name": "project_flock_id",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Comma separated area ids.",
"example": "1,2",
"in": "query",
"name": "area_id",
"required": false,
"schema": {
"type": "string"
}
},
{
"description": "Comma separated location ids.",
"example": "1,2",
"in": "query",
"name": "location_id",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedEnvelope"
}
}
},
"description": "Successful response"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorEnvelope"
}
}
},
"description": "Forbidden"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"BearerAuth": []
}
],
"summary": "GET api / reports / expense / depreciation / manual inputs",
"tags": [
"Reports"
]
}
},
"/api/reports/hpp-per-kandang": { "/api/reports/hpp-per-kandang": {
"get": { "get": {
"description": "Read access to `/api/reports/hpp-per-kandang`.", "description": "Read access to `/api/reports/hpp-per-kandang`.",
+135
View File
@@ -5318,6 +5318,141 @@ paths:
summary: GET api / reports / expense summary: GET api / reports / expense
tags: tags:
- Reports - Reports
/api/reports/expense/depreciation:
get:
description: Read access to `/api/reports/expense/depreciation`.
parameters:
- description: Page number.
example: 1
in: query
name: page
required: false
schema:
type: integer
- description: Page size.
example: 10
in: query
name: limit
required: false
schema:
type: integer
- description: Daily period filter (YYYY-MM-DD).
example: "2026-01-01"
in: query
name: period
required: true
schema:
type: string
- description: Comma separated project flock ids.
example: 1,2
in: query
name: project_flock_id
required: false
schema:
type: string
- description: Comma separated area ids.
example: 1,2
in: query
name: area_id
required: false
schema:
type: string
- description: Comma separated location ids.
example: 1,2
in: query
name: location_id
required: false
schema:
type: string
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedEnvelope'
description: Successful response
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
description: Forbidden
security:
- ApiKeyAuth: []
- BearerAuth: []
summary: GET api / reports / expense / depreciation
tags:
- Reports
/api/reports/expense/depreciation/manual-inputs:
get:
description: Read access to `/api/reports/expense/depreciation/manual-inputs`.
parameters:
- description: Page number.
example: 1
in: query
name: page
required: false
schema:
type: integer
- description: Page size.
example: 10
in: query
name: limit
required: false
schema:
type: integer
- description: Comma separated project flock ids.
example: 1,2
in: query
name: project_flock_id
required: false
schema:
type: string
- description: Comma separated area ids.
example: 1,2
in: query
name: area_id
required: false
schema:
type: string
- description: Comma separated location ids.
example: 1,2
in: query
name: location_id
required: false
schema:
type: string
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedEnvelope'
description: Successful response
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
description: Forbidden
security:
- ApiKeyAuth: []
- BearerAuth: []
summary: GET api / reports / expense / depreciation / manual inputs
tags:
- Reports
/api/reports/hpp-per-kandang: /api/reports/hpp-per-kandang:
get: get:
description: Read access to `/api/reports/hpp-per-kandang`. description: Read access to `/api/reports/hpp-per-kandang`.
+26
View File
@@ -1439,6 +1439,32 @@
"url": "{{base_url}}/api/reports/expense?page=1\u0026limit=10\u0026search=operasional\u0026category=BOP\u0026supplier_id=1\u0026kandang_id=1\u0026project_flock_kandang_id=1\u0026nonstock_id=1\u0026location_id=1\u0026area_id=1\u0026realization_date=2026-01-15" "url": "{{base_url}}/api/reports/expense?page=1\u0026limit=10\u0026search=operasional\u0026category=BOP\u0026supplier_id=1\u0026kandang_id=1\u0026project_flock_kandang_id=1\u0026nonstock_id=1\u0026location_id=1\u0026area_id=1\u0026realization_date=2026-01-15"
} }
}, },
{
"name": "GET api / reports / expense / depreciation",
"request": {
"header": [
{
"key": "Accept",
"value": "application/json"
}
],
"method": "GET",
"url": "{{base_url}}/api/reports/expense/depreciation?page=1\u0026limit=10\u0026period=2026-01-01\u0026project_flock_id=1,2\u0026area_id=1,2\u0026location_id=1,2"
}
},
{
"name": "GET api / reports / expense / depreciation / manual inputs",
"request": {
"header": [
{
"key": "Accept",
"value": "application/json"
}
],
"method": "GET",
"url": "{{base_url}}/api/reports/expense/depreciation/manual-inputs?page=1\u0026limit=10\u0026project_flock_id=1,2\u0026area_id=1,2\u0026location_id=1,2"
}
},
{ {
"name": "GET api / reports / hpp per kandang", "name": "GET api / reports / hpp per kandang",
"request": { "request": {
+1
View File
@@ -82,6 +82,7 @@ func DefaultDashboardPermissions() []string {
"lti.repport.debtsupplier.list", "lti.repport.debtsupplier.list",
"lti.repport.delivery.list", "lti.repport.delivery.list",
"lti.repport.expense.list", "lti.repport.expense.list",
"lti.repport.expense.depreciation.manage",
"lti.repport.gethppperkandang.list", "lti.repport.gethppperkandang.list",
"lti.repport.production_result.list", "lti.repport.production_result.list",
"lti.repport.purchasesupplier.list", "lti.repport.purchasesupplier.list",
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -23,6 +24,7 @@ type HppCostRepository interface {
GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error)
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
GetManualDepreciationCostByProjectFlockID(ctx context.Context, projectFlockId uint) (float64, error)
} }
type HppRepositoryImpl struct { type HppRepositoryImpl struct {
@@ -48,12 +50,32 @@ func (r *HppRepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, proje
} }
func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
var total float64 var total float64
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). Select(`
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String(), entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin). COALESCE(SUM(sa.qty * CASE
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0
END), 0)`,
stockablePurchase,
stockableAdjustment,
).
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?",
usableProjectChickin,
stockablePurchase,
stockableAdjustment,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeTraceChickin,
).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
@@ -85,7 +107,7 @@ func (r *HppRepositoryImpl) GetExpedisionCost(ctx context.Context, projectFlockK
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id"). Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock). Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs). Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagEkspedisi). // Where("f.name = ?", utils.FlagEkspedisi).
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
return 0, err return 0, err
@@ -100,15 +122,35 @@ func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKa
date = &now date = &now
} }
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
usableRecordingStock := fifo.UsableKeyRecordingStock.String()
var total float64 var total float64
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). Select(`
COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0
END), 0)`,
stockablePurchase,
stockableAdjustment,
).
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
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(
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?",
usableRecordingStock,
stockablePurchase,
stockableAdjustment,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date). Where("r.record_datetime <= ?", *date).
Where("f.name = ?", utils.FlagPakan). Where("f.name = ?", utils.FlagPakan).
@@ -132,15 +174,34 @@ func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKan
utils.FlagVitamin, utils.FlagVitamin,
utils.FlagKimia, utils.FlagKimia,
} }
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
usableRecordingStock := fifo.UsableKeyRecordingStock.String()
var total float64 var total float64
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("recordings AS r"). Table("recordings AS r").
Select("COALESCE(SUM(sa.qty * COALESCE(pi.price, 0)), 0)"). Select(`
COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
ELSE 0
END), 0)`,
stockablePurchase,
stockableAdjustment,
).
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id"). Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_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(
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id"). "JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND (sa.stockable_type = ? OR sa.stockable_type = ?) AND sa.status = ? AND sa.allocation_purpose = ?",
usableRecordingStock,
stockablePurchase,
stockableAdjustment,
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date). Where("r.record_datetime <= ?", *date).
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags). Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
@@ -169,22 +230,28 @@ func (r *HppRepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlock
func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) { func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) {
stockablePurchase := fifo.StockableKeyPurchaseItems.String() stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableTransferIn := fifo.StockableKeyStockTransferIn.String() stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
stockableAdjustment := fifo.StockableKeyAdjustmentIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String() usableProjectChickin := fifo.UsableKeyProjectChickin.String()
var total float64 var total float64
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
COALESCE(SUM(sa.qty * CASE COALESCE(SUM(sa.qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0)
ELSE 0 WHEN sa.stockable_type = ? THEN COALESCE(ast.price, 0)
END), 0)`, ELSE 0
stockablePurchase, stockableTransferIn). END), 0)`,
stockablePurchase,
stockableTransferIn,
stockableAdjustment,
).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", usableProjectChickin, entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin). Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.status = ? AND sa.allocation_purpose = ?", usableProjectChickin, entity.StockAllocationStatusActive, entity.StockAllocationPurposeTraceChickin).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ? AND tsa.status = ? AND tsa.allocation_purpose = ?", stockableTransferIn, stockableTransferIn, stockablePurchase, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume). Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ? AND tsa.status = ? AND tsa.allocation_purpose = ?", stockableTransferIn, stockableTransferIn, stockablePurchase, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume).
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id"). Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id").
Joins("LEFT JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", stockableAdjustment).
Where("pc.project_flock_kandang_id = ?", projectFlockKandangId). Where("pc.project_flock_kandang_id = ?", projectFlockKandangId).
Scan(&total).Error Scan(&total).Error
if err != nil { if err != nil {
@@ -215,6 +282,33 @@ func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandang
return 0, 0, err return 0, 0, err
} }
var adjustmentTotalWeight float64
adjustmentSubQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT ast.id AS adjustment_id, ast.price AS price").
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Joins("JOIN stock_transfer_details AS std ON std.dest_product_warehouse_id = re.product_warehouse_id").
Joins(
"JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = std.id AND sa.stockable_type = ? AND sa.status = ? AND sa.allocation_purpose = ?",
fifo.UsableKeyStockTransferOut.String(),
fifo.StockableKeyAdjustmentIn.String(),
entity.StockAllocationStatusActive,
entity.StockAllocationPurposeConsume,
).
Joins("JOIN adjustment_stocks AS ast ON ast.id = sa.stockable_id AND ast.product_warehouse_id = std.source_product_warehouse_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date)
err = r.db.WithContext(ctx).
Table("(?) AS adjustment_sources", adjustmentSubQuery).
Select("COALESCE(SUM(adjustment_sources.price), 0)").
Scan(&adjustmentTotalWeight).Error
if err != nil {
return 0, 0, err
}
totals.TotalWeightKg += adjustmentTotalWeight
return totals.TotalPieces, totals.TotalWeightKg, nil return totals.TotalPieces, totals.TotalWeightKg, nil
} }
@@ -311,3 +405,25 @@ func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projec
return summary.ProjectFlockID, summary.TotalQty, nil return summary.ProjectFlockID, summary.TotalQty, nil
} }
func (r *HppRepositoryImpl) GetManualDepreciationCostByProjectFlockID(ctx context.Context, projectFlockId uint) (float64, error) {
type row struct {
TotalCost float64
}
var selected row
err := r.db.WithContext(ctx).
Table("farm_depreciation_manual_inputs").
Select("total_cost").
Where("project_flock_id = ?", projectFlockId).
Limit(1).
Take(&selected).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
if err != nil {
return 0, err
}
return selected.TotalCost, nil
}
+186 -14
View File
@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"log"
"math" "math"
"time" "time"
@@ -39,77 +40,108 @@ func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
} }
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) { func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
logHpp("CalculateHppCost", "start project_flock_kandang_id=%d input_date=%s", projectFlockKandangId, formatTimePtr(date))
if date == nil { if date == nil {
now := time.Now() now := time.Now()
date = &now date = &now
} }
logHpp("CalculateHppCost", "normalized_date=%s", formatTimePtr(date))
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
logHpp("CalculateHppCost", "load_location_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "location=%s", location.String())
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location) startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour) endOfDay := startOfDay.Add(24 * time.Hour)
logHpp("CalculateHppCost", "start_of_day=%s end_of_day=%s", startOfDay.Format(time.RFC3339), endOfDay.Format(time.RFC3339))
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay) depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil { if err != nil {
logHpp("CalculateHppCost", "get_depresiasi_transfer_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "depresiasi_transfer=%f", depresiasiTransfer)
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer) totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer)
if err != nil { if err != nil {
logHpp("CalculateHppCost", "get_total_production_cost_error=%v", err)
return nil, err return nil, err
} }
logHpp("CalculateHppCost", "total_production_cost=%f", totalProductionCost)
return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay) result, err := s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
if err != nil {
logHpp("CalculateHppCost", "get_hpp_estimation_dan_realisasi_error=%v", err)
return nil, err
}
logHpp("CalculateHppCost", "done estimation=%+v real=%+v", result.Estimation, result.Real)
return result, nil
} }
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) { func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) {
logHpp("GetTotalDepresiasiFlockGrowing", "start source_project_flock_id=%d input_date=%s", sourceProjectFlockID, formatTimePtr(date))
if date == nil { if date == nil {
now := time.Now() now := time.Now()
date = &now date = &now
} }
logHpp("GetTotalDepresiasiFlockGrowing", "normalized_date=%s", formatTimePtr(date))
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetTotalDepresiasiFlockGrowing", "repo_nil return=0")
return 0, nil return 0, nil
} }
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "kandang_ids=%v", kandangIDs)
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs) docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_doc_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "doc_cost=%f", docCost)
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID) budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_budget_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "budget_cost=%f", budgetCost)
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs) expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_expedision_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "expedision_cost=%f", expedisionCost)
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date) feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_feed_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "feed_cost=%f", feedCost)
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date) ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
if err != nil { if err != nil {
logHpp("GetTotalDepresiasiFlockGrowing", "get_ovk_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalDepresiasiFlockGrowing", "ovk_cost=%f", ovkCost)
return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil total := docCost + budgetCost + expedisionCost + feedCost + ovkCost
logHpp("GetTotalDepresiasiFlockGrowing", "done total=%f", total)
return total, nil
} }
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) { func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) {
logHpp("GetTotalProductionCost", "start project_flock_kandang_id=%d end_date=%s depresiasi_transfer=%f", projectFlockKandangId, formatTimePtr(endDate), depresiasiTransfer)
// if date == nil { // if date == nil {
// now := time.Now() // now := time.Now()
// date = &now // date = &now
@@ -117,125 +149,248 @@ func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId) costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_pullet_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_pullet=%f", costPullet)
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_feed_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_feed=%f", costFeed)
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate) costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_ovk_usage_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_ovk=%f", costOvk)
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId}) costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_expedision_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_expedision=%f", costExpedision)
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate) costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate)
if err != nil { if err != nil {
logHpp("GetTotalProductionCost", "get_budget_kandang_laying_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetTotalProductionCost", "cost_budget=%f", costBudget)
return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil // fmt.Println(costBudget, costExpedision, costOvk, costFeed, costPullet, depresiasiTransfer)
// depresiasiTransfer = 0
total := depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget
logHpp("GetTotalProductionCost", "done total=%f", total)
return total, nil
} }
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) { func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
logHpp("GetBudgetKandangLaying", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate))
// if date == nil { // if date == nil {
// now := time.Now() // now := time.Now()
// date = &now // date = &now
// } // }
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetBudgetKandangLaying", "repo_nil return=0")
return 0, nil return 0, nil
} }
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId) projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_id_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "project_flock_id=%d", projectFlockId)
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId) projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "project_flock_kandang_ids=%v", projectFlockKandangIds)
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate) eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_flock_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock=%f", eggProduksiPiecesFlock)
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_egg_produksi_pieces_kandang_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_kandang=%f", eggProduksiPiecesKandang)
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId) totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
if err != nil { if err != nil {
logHpp("GetBudgetKandangLaying", "get_budget_cost_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetBudgetKandangLaying", "total_budget_cost=%f", totalBudgetCost)
if eggProduksiPiecesFlock == 0 { if eggProduksiPiecesFlock == 0 {
logHpp("GetBudgetKandangLaying", "egg_produksi_pieces_flock_zero return=0")
return 0, nil return 0, nil
} }
return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil result := (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock
logHpp("GetBudgetKandangLaying", "done result=%f", result)
return result, nil
} }
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) { func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
// if endDate == nil { logHpp("GetDepresiasiTransfer", "start project_flock_kandang_id=%d end_date=%s", projectFlockKandangId, formatTimePtr(endDate))
// now := time.Now() if endDate == nil {
// endDate = &now now := time.Now()
// } endDate = &now
}
logHpp("GetDepresiasiTransfer", "normalized_end_date=%s", formatTimePtr(endDate))
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetDepresiasiTransfer", "repo_nil return=0")
return 0, nil return 0, nil
} }
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId) sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_transfer_source_summary_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "source_project_flock_id=%d transfer_total_qty=%f", sourceProjectFlockID, transferTotalQty)
if sourceProjectFlockID == 0 || transferTotalQty <= 0 {
logHpp("GetDepresiasiTransfer", "use_manual_fallback=true")
result, fallbackErr := s.getManualDepresiasiTransferFallback(projectFlockKandangId)
if fallbackErr != nil {
logHpp("GetDepresiasiTransfer", "manual_fallback_error=%v", fallbackErr)
return 0, fallbackErr
}
logHpp("GetDepresiasiTransfer", "done_fallback result=%f", result)
return result, nil
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID) kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_project_flock_kandang_ids_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "kandang_ids_growing=%v", kandangIDsGrowing)
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing) totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_population_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "total_population_flock_growing=%f", totalPopulationFlockGrowing)
if totalPopulationFlockGrowing == 0 { if totalPopulationFlockGrowing == 0 {
logHpp("GetDepresiasiTransfer", "total_population_flock_growing_zero return=0")
return 0, nil return 0, nil
} }
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate) totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate)
if err != nil { if err != nil {
logHpp("GetDepresiasiTransfer", "get_total_depresiasi_flock_growing_error=%v", err)
return 0, err return 0, err
} }
logHpp("GetDepresiasiTransfer", "total_depresiasi_flock_growing=%f", totalDepresiasiFlockGrowing)
return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil result := (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing
logHpp("GetDepresiasiTransfer", "done result=%f", result)
return result, nil
}
func (s *hppService) getManualDepresiasiTransferFallback(projectFlockKandangId uint) (float64, error) {
logHpp("getManualDepresiasiTransferFallback", "start project_flock_kandang_id=%d", projectFlockKandangId)
projectFlockID, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_id_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "project_flock_id=%d", projectFlockID)
if projectFlockID == 0 {
logHpp("getManualDepresiasiTransferFallback", "project_flock_id_zero return=0")
return 0, nil
}
manualCost, err := s.hppRepo.GetManualDepreciationCostByProjectFlockID(context.Background(), projectFlockID)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_manual_depreciation_cost_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "manual_cost=%f", manualCost)
if manualCost <= 0 {
logHpp("getManualDepresiasiTransferFallback", "manual_cost_non_positive return=0")
return 0, nil
}
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockID)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_project_flock_kandang_ids_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "kandang_ids=%v", kandangIDs)
if len(kandangIDs) == 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_ids_empty return=0")
return 0, nil
}
totalUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDs)
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_total_usage_qty_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty=%f", totalUsageQty)
if totalUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "total_usage_qty_non_positive return=0")
return 0, nil
}
kandangUsageQty, err := s.hppRepo.GetTotalPopulation(context.Background(), []uint{projectFlockKandangId})
if err != nil {
logHpp("getManualDepresiasiTransferFallback", "get_kandang_usage_qty_error=%v", err)
return 0, err
}
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty=%f", kandangUsageQty)
if kandangUsageQty <= 0 {
logHpp("getManualDepresiasiTransferFallback", "kandang_usage_qty_non_positive return=0")
return 0, nil
}
result := manualCost * (kandangUsageQty / totalUsageQty)
logHpp("getManualDepresiasiTransferFallback", "done result=%f", result)
return result, nil
} }
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) { func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
logHpp("GetHppEstimationDanRealisasi", "start total_production_cost=%f project_flock_kandang_id=%d start_date=%s end_date=%s", totalProductionCost, projectFlockKandangId, formatTimePtr(startDate), formatTimePtr(endDate))
if s.hppRepo == nil { if s.hppRepo == nil {
logHpp("GetHppEstimationDanRealisasi", "repo_nil return_empty_response")
return &HppCostResponse{}, nil return &HppCostResponse{}, nil
} }
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate) estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil { if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_produksi_error=%v", err)
return nil, err return nil, err
} }
logHpp("GetHppEstimationDanRealisasi", "estim_pieces=%f estim_weight_kg=%f", estimPieces, estimWeightKg)
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate) realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil { if err != nil {
logHpp("GetHppEstimationDanRealisasi", "get_egg_terjual_error=%v", err)
return nil, err return nil, err
} }
logHpp("GetHppEstimationDanRealisasi", "real_pieces=%f real_weight_kg=%f", realPieces, realWeightKg)
estimation := HppCostDetail{ estimation := HppCostDetail{
Total: totalProductionCost, Total: totalProductionCost,
@@ -248,6 +403,7 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if estimPieces > 0 { if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces) estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
} }
logHpp("GetHppEstimationDanRealisasi", "estimation=%+v", estimation)
real := HppCostDetail{ real := HppCostDetail{
Total: totalProductionCost, Total: totalProductionCost,
@@ -260,13 +416,29 @@ func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, p
if realPieces > 0 { if realPieces > 0 {
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces) real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
} }
logHpp("GetHppEstimationDanRealisasi", "real=%+v", real)
return &HppCostResponse{ result := &HppCostResponse{
Estimation: estimation, Estimation: estimation,
Real: real, Real: real,
}, nil }
logHpp("GetHppEstimationDanRealisasi", "done response=%+v", *result)
return result, nil
} }
func roundToTwoDecimals(value float64) float64 { func roundToTwoDecimals(value float64) float64 {
return math.Round(value*100) / 100 result := math.Round(value*100) / 100
logHpp("roundToTwoDecimals", "input=%f output=%f", value, result)
return result
}
func formatTimePtr(value *time.Time) string {
if value == nil {
return "<nil>"
}
return value.Format(time.RFC3339)
}
func logHpp(method, format string, args ...any) {
log.Printf("[HPP][%s] "+format, append([]any{method}, args...)...)
} }
@@ -0,0 +1,103 @@
package service
import (
"context"
"time"
"gorm.io/gorm"
)
const farmDepreciationSnapshotTable = "farm_depreciation_snapshots"
func NormalizeDateOnlyUTC(value time.Time) time.Time {
if value.IsZero() {
return value
}
v := value.UTC()
return time.Date(v.Year(), v.Month(), v.Day(), 0, 0, 0, 0, time.UTC)
}
func MinNonZeroDateOnlyUTC(values ...time.Time) time.Time {
var out time.Time
for _, value := range values {
if value.IsZero() {
continue
}
normalized := NormalizeDateOnlyUTC(value)
if out.IsZero() || normalized.Before(out) {
out = normalized
}
}
return out
}
func InvalidateFarmDepreciationSnapshotsFromDate(ctx context.Context, db *gorm.DB, farmIDs []uint, fromDate time.Time) error {
if db == nil {
return nil
}
if fromDate.IsZero() {
return nil
}
fromDate = NormalizeDateOnlyUTC(fromDate)
query := db.WithContext(ctx).
Table(farmDepreciationSnapshotTable).
Where("period_date >= ?", fromDate)
if len(farmIDs) > 0 {
query = query.Where("project_flock_id IN ?", farmIDs)
}
return query.Delete(nil).Error
}
func ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx context.Context, db *gorm.DB, pfkIDs []uint) ([]uint, error) {
if db == nil || len(pfkIDs) == 0 {
return []uint{}, nil
}
var projectFlockIDs []uint
if err := db.WithContext(ctx).
Table("project_flock_kandangs").
Distinct("project_flock_id").
Where("id IN ?", pfkIDs).
Pluck("project_flock_id", &projectFlockIDs).Error; err != nil {
return nil, err
}
return projectFlockIDs, nil
}
func ResolveProjectFlockIDsByExpenseID(ctx context.Context, db *gorm.DB, expenseID uint) ([]uint, error) {
if db == nil || expenseID == 0 {
return []uint{}, nil
}
query := `
WITH direct_farms AS (
SELECT DISTINCT pfk.project_flock_id
FROM expense_nonstocks ens
JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id
WHERE ens.expense_id = @expense_id
),
json_farms AS (
SELECT DISTINCT (jsonb_array_elements_text(e.project_flock_id::jsonb))::bigint AS project_flock_id
FROM expenses e
WHERE e.id = @expense_id
AND e.project_flock_id IS NOT NULL
)
SELECT DISTINCT project_flock_id
FROM (
SELECT project_flock_id FROM direct_farms
UNION ALL
SELECT project_flock_id FROM json_farms
) x
`
var ids []uint
if err := db.WithContext(ctx).Raw(query, map[string]any{
"expense_id": expenseID,
}).Scan(&ids).Error; err != nil {
return nil, err
}
return ids, nil
}
@@ -0,0 +1,6 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS house_type;
DROP TABLE IF EXISTS house_depreciation_standards;
DROP TYPE IF EXISTS house_type_enum;
@@ -0,0 +1,18 @@
CREATE TYPE house_type_enum AS ENUM ('open_house', 'close_house');
CREATE TABLE house_depreciation_standards (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100),
effective_date DATE,
house_type house_type_enum NOT NULL,
day INT NOT NULL
CHECK (day >= 0),
depreciation_percent NUMERIC(15, 6) NOT NULL
CHECK (depreciation_percent >= 0 AND depreciation_percent <= 100),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT house_depreciation_standards_house_type_day_unique UNIQUE (house_type, day)
);
ALTER TABLE kandangs
ADD COLUMN house_type house_type_enum;
@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_farm_depreciation_snapshots_project_flock_id;
DROP INDEX IF EXISTS idx_farm_depreciation_snapshots_period_date;
DROP TABLE IF EXISTS farm_depreciation_snapshots;
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS farm_depreciation_snapshots (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL
REFERENCES project_flocks(id)
ON UPDATE CASCADE
ON DELETE CASCADE,
period_date DATE NOT NULL,
depreciation_percent_effective NUMERIC(15, 6) NOT NULL DEFAULT 0,
depreciation_value NUMERIC(18, 3) NOT NULL DEFAULT 0,
pullet_cost_day_n_total NUMERIC(18, 3) NOT NULL DEFAULT 0,
components JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT farm_depreciation_snapshots_unique UNIQUE (project_flock_id, period_date)
);
CREATE INDEX IF NOT EXISTS idx_farm_depreciation_snapshots_period_date
ON farm_depreciation_snapshots (period_date);
CREATE INDEX IF NOT EXISTS idx_farm_depreciation_snapshots_project_flock_id
ON farm_depreciation_snapshots (project_flock_id);
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_farm_depreciation_manual_inputs_project_flock_id;
DROP TABLE IF EXISTS farm_depreciation_manual_inputs;
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS farm_depreciation_manual_inputs (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL
REFERENCES project_flocks(id)
ON UPDATE CASCADE
ON DELETE CASCADE,
total_cost NUMERIC(18, 3) NOT NULL DEFAULT 0
CHECK (total_cost >= 0),
note TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT farm_depreciation_manual_inputs_unique UNIQUE (project_flock_id)
);
CREATE INDEX IF NOT EXISTS idx_farm_depreciation_manual_inputs_project_flock_id
ON farm_depreciation_manual_inputs (project_flock_id);
@@ -0,0 +1,17 @@
package entities
import "time"
type FarmDepreciationManualInput struct {
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_manual_inputs_unique"`
TotalCost float64 `gorm:"type:numeric(18,3);not null;default:0"`
Note *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
}
func (FarmDepreciationManualInput) TableName() string {
return "farm_depreciation_manual_inputs"
}
@@ -0,0 +1,21 @@
package entities
import (
"time"
)
type FarmDepreciationSnapshot struct {
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;uniqueIndex:idx_farm_depreciation_snapshots_unique,priority:1"`
PeriodDate time.Time `gorm:"type:date;not null;uniqueIndex:idx_farm_depreciation_snapshots_unique,priority:2"`
DepreciationPercentEffective float64 `gorm:"type:numeric(15,6);not null;default:0"`
DepreciationValue float64 `gorm:"type:numeric(18,3);not null;default:0"`
PulletCostDayNTotal float64 `gorm:"type:numeric(18,3);not null;default:0"`
Components []byte `gorm:"type:jsonb;default:'{}'::jsonb"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (FarmDepreciationSnapshot) TableName() string {
return "farm_depreciation_snapshots"
}
@@ -0,0 +1,16 @@
package entities
import "time"
type HouseDepreciationStandard struct {
Id uint `gorm:"primaryKey"`
HouseType string `gorm:"type:house_type_enum;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:1"`
DayNumber int `gorm:"column:day;not null;uniqueIndex:house_depreciation_standards_house_type_day_unique,priority:2"`
DepreciationPercent float64 `gorm:"type:numeric(15,6);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (HouseDepreciationStandard) TableName() string {
return "house_depreciation_standards"
}
+1
View File
@@ -10,6 +10,7 @@ type Kandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"` Status string `gorm:"type:varchar(50);not null"`
HouseType *string `gorm:"type:house_type_enum"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
KandangGroupId uint `gorm:"not null"` KandangGroupId uint `gorm:"not null"`
Capacity float64 `gorm:"not null"` Capacity float64 `gorm:"not null"`
+8 -7
View File
@@ -47,13 +47,14 @@ const (
P_ApprovalGetAll = "lti.approval.list" P_ApprovalGetAll = "lti.approval.list"
) )
const ( const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportExpenseDepreciationManage = "lti.repport.expense.depreciation.manage"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list" P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list" P_ReportProductionResultGetAll = "lti.repport.production_result.list"
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
) )
const ( const (
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"time"
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"
@@ -358,6 +359,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, uint(expense.Id), expenseDate, nil)
return responseDTO, nil return responseDTO, nil
} }
@@ -385,6 +387,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
updateBody := make(map[string]any) updateBody := make(map[string]any)
var requestedTransactionDate *time.Time
if req.TransactionDate != nil { if req.TransactionDate != nil {
expenseDate, err := utils.ParseDateString(*req.TransactionDate) expenseDate, err := utils.ParseDateString(*req.TransactionDate)
@@ -392,6 +395,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
} }
updateBody["transaction_date"] = expenseDate updateBody["transaction_date"] = expenseDate
requestedTransactionDate = &expenseDate
} }
if req.Category != nil { if req.Category != nil {
@@ -429,6 +433,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return responseDTO, nil return responseDTO, nil
} }
var invalidationFromDate time.Time
var invalidationFarmIDs []uint
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
expenseRepoTx := repository.NewExpenseRepository(tx) expenseRepoTx := repository.NewExpenseRepository(tx)
@@ -446,6 +452,16 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil { if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
return err return err
} }
oldFarmIDs, resolveOldFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), tx, id)
if resolveOldFarmErr != nil {
s.Log.Warnf("Failed to resolve old expense farm ids for invalidation (expense_id=%d): %+v", id, resolveOldFarmErr)
}
invalidationFarmIDs = append(invalidationFarmIDs, oldFarmIDs...)
invalidationFromDate = currentExpense.TransactionDate
if requestedTransactionDate != nil {
invalidationFromDate = commonSvc.MinNonZeroDateOnlyUTC(currentExpense.TransactionDate, *requestedTransactionDate)
}
categoryChanged := false categoryChanged := false
var newCategory string var newCategory string
if req.Category != nil && *req.Category != currentExpense.Category { if req.Category != nil && *req.Category != currentExpense.Category {
@@ -631,6 +647,12 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
} }
newFarmIDs, resolveNewFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), tx, id)
if resolveNewFarmErr != nil {
s.Log.Warnf("Failed to resolve new expense farm ids for invalidation (expense_id=%d): %+v", id, resolveNewFarmErr)
}
invalidationFarmIDs = append(invalidationFarmIDs, newFarmIDs...)
return nil return nil
}) })
@@ -645,6 +667,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.invalidateDepreciationSnapshots(c.Context(), nil, invalidationFarmIDs, invalidationFromDate)
return responseDTO, nil return responseDTO, nil
} }
@@ -671,6 +694,10 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint64) error {
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil { if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
return err return err
} }
farmIDs, resolveFarmErr := commonSvc.ResolveProjectFlockIDsByExpenseID(c.Context(), s.Repository.DB(), idUint)
if resolveFarmErr != nil {
s.Log.Warnf("Failed to resolve expense farm ids before delete (expense_id=%d): %+v", idUint, resolveFarmErr)
}
if err := s.Repository.DeleteOne(c.Context(), idUint); err != nil { if err := s.Repository.DeleteOne(c.Context(), idUint); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Expense not found for ID %d: %+v", id, err) s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
@@ -680,6 +707,8 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint64) error {
return err return err
} }
s.Log.Infof("Successfully deleted expense with ID %d", id) s.Log.Infof("Successfully deleted expense with ID %d", id)
invalidationFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate)
s.invalidateDepreciationSnapshots(c.Context(), nil, farmIDs, invalidationFromDate)
return nil return nil
} }
@@ -800,6 +829,8 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
if err != nil { if err != nil {
return nil, err return nil, err
} }
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, realizationDate, expense.RealizationDate)
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil)
return responseDTO, nil return responseDTO, nil
} }
@@ -857,6 +888,13 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
expense, expenseErr := s.Repository.GetByID(c.Context(), id, nil)
if expenseErr != nil {
s.Log.Warnf("Failed to load expense for depreciation invalidation after complete (expense_id=%d): %+v", id, expenseErr)
} else {
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate)
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, id, invalidateFromDate, nil)
}
return responseDTO, nil return responseDTO, nil
} }
@@ -884,6 +922,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil { if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
return nil, err return nil, err
} }
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(expense.TransactionDate, expense.RealizationDate)
if req.RealizationDate != nil {
if parsedDate, parseErr := utils.ParseDateString(*req.RealizationDate); parseErr == nil {
invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, parsedDate)
}
}
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil) latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
@@ -996,6 +1040,7 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil)
return responseDTO, nil return responseDTO, nil
} }
@@ -1057,6 +1102,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
} }
var results []expenseDto.ExpenseDetailDTO var results []expenseDto.ExpenseDetailDTO
invalidateFromDateByExpenseID := make(map[uint]time.Time)
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
@@ -1069,6 +1115,17 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
); err != nil { ); err != nil {
return err return err
} }
expenseForInvalidation, err := expenseRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
}
invalidateFromDateByExpenseID[id] = commonSvc.MinNonZeroDateOnlyUTC(
expenseForInvalidation.TransactionDate,
expenseForInvalidation.RealizationDate,
)
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil) latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1170,10 +1227,73 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed approve expenses") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed approve expenses")
} }
for expenseID, invalidateFromDate := range invalidateFromDateByExpenseID {
s.invalidateDepreciationSnapshotsByExpense(c.Context(), nil, expenseID, invalidateFromDate, nil)
}
return results, nil return results, nil
} }
func (s *expenseService) invalidateDepreciationSnapshotsByExpense(
ctx context.Context,
tx *gorm.DB,
expenseID uint,
fromDate time.Time,
fallbackFarmIDs []uint,
) {
targetDB := s.Repository.DB()
if tx != nil {
targetDB = tx
}
farmIDs := append([]uint{}, fallbackFarmIDs...)
if expenseID != 0 {
resolvedFarmIDs, err := commonSvc.ResolveProjectFlockIDsByExpenseID(ctx, targetDB, expenseID)
if err != nil {
s.Log.Warnf("Failed to resolve expense farm ids for invalidation (expense_id=%d): %+v", expenseID, err)
} else {
farmIDs = append(farmIDs, resolvedFarmIDs...)
}
}
s.invalidateDepreciationSnapshots(ctx, tx, farmIDs, fromDate)
}
func (s *expenseService) invalidateDepreciationSnapshots(
ctx context.Context,
tx *gorm.DB,
farmIDs []uint,
fromDate time.Time,
) {
if fromDate.IsZero() {
return
}
targetDB := s.Repository.DB()
if tx != nil {
targetDB = tx
}
farmIDs = utils.UniqueUintSlice(farmIDs)
if len(farmIDs) == 0 {
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots globally (from=%s): %+v",
fromDate.Format("2006-01-02"),
err,
)
}
return
}
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v",
farmIDs,
fromDate.Format("2006-01-02"),
err,
)
}
}
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) { func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
expenseRepoTx := repository.NewExpenseRepository(ctx) expenseRepoTx := repository.NewExpenseRepository(ctx)
@@ -419,6 +419,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
if len(result) == 0 { if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
} }
invalidateFromDate := time.Time{}
for i := range result {
invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, result[i].ChickInDate)
}
s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{req.ProjectFlockKandangId}, invalidateFromDate)
return result, nil return result, nil
} }
@@ -462,6 +467,8 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(chickin.ChickInDate, updated.ChickInDate)
s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{updated.ProjectFlockKandangId}, invalidateFromDate)
if updated.UsageQty > 0 { if updated.UsageQty > 0 {
if err := s.syncChickinTraceForProductWarehouse(c.Context(), nil, updated.ProductWarehouseId); err != nil { if err := s.syncChickinTraceForProductWarehouse(c.Context(), nil, updated.ProductWarehouseId); err != nil {
@@ -566,6 +573,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
consumeAllocAfter, consumeAllocAfter,
traceAllocAfter, traceAllocAfter,
) )
s.invalidateDepreciationSnapshots(c.Context(), tx, []uint{lockedChickin.ProjectFlockKandangId}, lockedChickin.ChickInDate)
return nil return nil
}) })
@@ -1160,6 +1168,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if action == entity.ApprovalActionApproved { if action == entity.ApprovalActionApproved {
step = utils.ChickinStepDisetujui step = utils.ChickinStepDisetujui
} }
invalidateFromByPFK := make(map[uint]time.Time, len(approvableIDs))
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 {
if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil { if err := s.ensurePopulationRouteScope(c.Context(), dbTransaction); err != nil {
@@ -1204,6 +1213,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
} }
for _, chickin := range chickins {
invalidateFromByPFK[approvableID] = commonSvc.MinNonZeroDateOnlyUTC(
invalidateFromByPFK[approvableID],
chickin.ChickInDate,
)
}
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil { if err != nil {
@@ -1281,6 +1296,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
} }
for _, chickin := range chickins {
invalidateFromByPFK[approvableID] = commonSvc.MinNonZeroDateOnlyUTC(
invalidateFromByPFK[approvableID],
chickin.ChickInDate,
)
}
if len(chickins) == 0 { if len(chickins) == 0 {
continue continue
@@ -1328,6 +1349,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
} }
for projectFlockKandangID, invalidateFromDate := range invalidateFromByPFK {
s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{projectFlockKandangID}, invalidateFromDate)
}
updated := make([]entity.ProjectChickin, 0) updated := make([]entity.ProjectChickin, 0)
for _, kandangID := range approvableIDs { for _, kandangID := range approvableIDs {
@@ -1837,6 +1861,57 @@ func normalizeDateOnlyUTC(value time.Time) time.Time {
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC) return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
} }
func (s chickinService) invalidateDepreciationSnapshots(
ctx context.Context,
tx *gorm.DB,
projectFlockKandangIDs []uint,
fromDate time.Time,
) {
if fromDate.IsZero() {
return
}
projectFlockKandangIDs = uniqueUint(projectFlockKandangIDs)
if len(projectFlockKandangIDs) == 0 {
return
}
targetDB := s.Repository.DB()
if tx != nil {
targetDB = tx
}
farmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, projectFlockKandangIDs)
if err != nil {
s.Log.Warnf(
"Failed to resolve farm ids for chickin depreciation invalidation (pfk_ids=%v): %+v",
projectFlockKandangIDs,
err,
)
farmIDs = nil
}
if len(farmIDs) == 0 {
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots globally (from=%s): %+v",
fromDate.Format("2006-01-02"),
err,
)
}
return
}
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v",
farmIDs,
fromDate.Format("2006-01-02"),
err,
)
}
}
func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error { func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error {
if productWarehouseID == 0 { if productWarehouseID == 0 {
return nil return nil
@@ -13,11 +13,11 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
ctrl := controller.NewProjectFlockKandangController(s) ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs") route := v1.Group("/project-flock-kandangs")
route.Use(m.Auth(u)) // route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne)
// route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing) // route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing)
// route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing) // route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing)
route.Post("/:id/closing",m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing) route.Post("/:id/closing", m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing)
route.Get("/:id/closing/check", m.RequirePermissions(m.P_ProjectFlockKandangsCheckClosing), ctrl.CheckClosing) route.Get("/:id/closing/check", m.RequirePermissions(m.P_ProjectFlockKandangsCheckClosing), ctrl.CheckClosing)
} }
@@ -517,7 +517,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return nil, transactionErr return nil, transactionErr
} }
return s.GetOne(c, createdRecording.Id) created, err := s.GetOne(c, createdRecording.Id)
if err != nil {
return nil, err
}
if created != nil {
s.invalidateDepreciationSnapshots(c.Context(), nil, created.ProjectFlockKandangId, created.RecordDatetime)
}
return created, nil
} }
func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) { func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) {
@@ -848,6 +855,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
if err := recordingutil.AttachProductionStandards(ctx, s.Repository.DB(), false, s.Log, updatedRecording); err != nil { if err := recordingutil.AttachProductionStandards(ctx, s.Repository.DB(), false, s.Log, updatedRecording); err != nil {
return nil, err return nil, err
} }
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(recordingEntity.RecordDatetime, updatedRecording.RecordDatetime)
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
updatedRecording.ProjectFlockKandangId,
invalidateFromDate,
)
return updatedRecording, nil return updatedRecording, nil
} }
@@ -965,6 +979,12 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
recording.ProjectFlockKandangId,
recording.RecordDatetime,
)
updated = append(updated, *recording) updated = append(updated, *recording)
} }
@@ -985,7 +1005,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
note := recordingutil.RecordingNote("Delete", id) note := recordingutil.RecordingNote("Delete", id)
return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { err = s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
recording, err := s.Repository.WithTx(tx).GetByID(ctx, id, nil) recording, err := s.Repository.WithTx(tx).GetByID(ctx, id, nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1029,9 +1049,60 @@ 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
} }
s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
return nil return nil
}) })
if err != nil {
return err
}
return nil
}
func (s recordingService) invalidateDepreciationSnapshots(
ctx context.Context,
tx *gorm.DB,
projectFlockKandangID uint,
fromDate time.Time,
) {
if projectFlockKandangID == 0 || fromDate.IsZero() {
return
}
targetDB := s.Repository.DB()
if tx != nil {
targetDB = tx
}
farmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, []uint{projectFlockKandangID})
if err != nil {
s.Log.Warnf(
"Failed to resolve farm for recording depreciation invalidation (pfk=%d): %+v",
projectFlockKandangID,
err,
)
farmIDs = nil
}
if len(farmIDs) == 0 {
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots globally (from=%s): %+v",
fromDate.Format("2006-01-02"),
err,
)
}
return
}
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v",
farmIDs,
fromDate.Format("2006-01-02"),
err,
)
}
} }
func (s *recordingService) resolveRecordingCategory(ctx context.Context, recording *entity.Recording) (string, error) { func (s *recordingService) resolveRecordingCategory(ctx context.Context, recording *entity.Recording) (string, error) {
@@ -377,6 +377,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{req.TargetProjectFlockId}, transferDate)
return laying_transfer, nil return laying_transfer, nil
} }
@@ -588,6 +589,13 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update,
} }
layingTransfer, _, err := s.GetOne(c, id) layingTransfer, _, err := s.GetOne(c, id)
invalidateFromDate := commonSvc.MinNonZeroDateOnlyUTC(existingTransfer.TransferDate, transferDate)
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
[]uint{existingTransfer.ToProjectFlockId, req.TargetProjectFlockId},
invalidateFromDate,
)
return layingTransfer, err return layingTransfer, err
} }
@@ -661,6 +669,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
s.Log.Errorf("Failed to delete transferLaying: %+v", err) s.Log.Errorf("Failed to delete transferLaying: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
} }
s.invalidateDepreciationSnapshots(c.Context(), nil, []uint{transfer.ToProjectFlockId}, transfer.TransferDate)
return nil return nil
} }
@@ -798,6 +807,14 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transfer != nil {
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
[]uint{transfer.ToProjectFlockId},
resolveDepreciationEffectiveDateForTransfer(transfer),
)
}
updated = append(updated, *transfer) updated = append(updated, *transfer)
} }
@@ -837,6 +854,14 @@ func (s transferLayingService) Execute(c *fiber.Ctx, id uint) (*entity.LayingTra
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transfer != nil {
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
[]uint{transfer.ToProjectFlockId},
resolveDepreciationEffectiveDateForTransfer(transfer),
)
}
return transfer, nil return transfer, nil
} }
@@ -873,6 +898,14 @@ func (s transferLayingService) ExecuteWithBusinessDate(c *fiber.Ctx, id uint, bu
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transfer != nil {
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
[]uint{transfer.ToProjectFlockId},
resolveDepreciationEffectiveDateForTransfer(transfer),
)
}
return transfer, nil return transfer, nil
} }
@@ -1226,6 +1259,14 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
if err != nil { if err != nil {
return nil, err return nil, err
} }
if transfer != nil {
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
[]uint{transfer.ToProjectFlockId},
resolveDepreciationEffectiveDateForTransfer(transfer),
)
}
return transfer, nil return transfer, nil
} }
@@ -1678,6 +1719,43 @@ func normalizeDateOnlyUTC(value time.Time) time.Time {
return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC) return time.Date(value.UTC().Year(), value.UTC().Month(), value.UTC().Day(), 0, 0, 0, 0, time.UTC)
} }
func resolveDepreciationEffectiveDateForTransfer(transfer *entity.LayingTransfer) time.Time {
if transfer == nil {
return time.Time{}
}
if transfer.EffectiveMoveDate != nil && !transfer.EffectiveMoveDate.IsZero() {
return *transfer.EffectiveMoveDate
}
if transfer.EconomicCutoffDate != nil && !transfer.EconomicCutoffDate.IsZero() {
return *transfer.EconomicCutoffDate
}
return transfer.TransferDate
}
func (s transferLayingService) invalidateDepreciationSnapshots(
ctx context.Context,
tx *gorm.DB,
farmIDs []uint,
fromDate time.Time,
) {
if fromDate.IsZero() {
return
}
targetDB := s.Repository.DB()
if tx != nil {
targetDB = tx
}
uniqueFarmIDs := utils.UniqueUintSlice(farmIDs)
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, uniqueFarmIDs, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate farm depreciation snapshots (farms=%v, from=%s): %+v",
uniqueFarmIDs,
fromDate.Format("2006-01-02"),
err,
)
}
}
func isLegacyTransfer(transfer *entity.LayingTransfer) bool { func isLegacyTransfer(transfer *entity.LayingTransfer) bool {
if transfer == nil { if transfer == nil {
return false return false
@@ -675,6 +675,12 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
if err := s.attachLatestApproval(c.Context(), created); err != nil { if err := s.attachLatestApproval(c.Context(), created); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", created.Id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", created.Id, err)
} }
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
collectPFKIDsFromPurchase(created),
resolvePurchaseDepreciationInvalidateDate(created, created.Items, now),
)
return created, nil return created, nil
} }
@@ -826,6 +832,12 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
if err := s.attachLatestApproval(c.Context(), updated); err != nil { if err := s.attachLatestApproval(c.Context(), updated); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
} }
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
collectPFKIDsFromPurchase(updated),
resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC()),
)
return updated, nil return updated, nil
} }
@@ -934,6 +946,12 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
if err := s.attachLatestApproval(c.Context(), updated); err != nil { if err := s.attachLatestApproval(c.Context(), updated); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
} }
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
collectPFKIDsFromPurchase(updated),
resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, now),
)
return updated, nil return updated, nil
} }
@@ -1421,6 +1439,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
if err := s.attachLatestApproval(c.Context(), updated); err != nil { if err := s.attachLatestApproval(c.Context(), updated); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
} }
invalidateFromDate := resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC())
if earliestReceived != nil {
invalidateFromDate = commonSvc.MinNonZeroDateOnlyUTC(invalidateFromDate, *earliestReceived)
}
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
collectPFKIDsFromPurchase(updated),
invalidateFromDate,
)
receivingPayloads := make([]ExpenseReceivingPayload, 0, len(prepared)) receivingPayloads := make([]ExpenseReceivingPayload, 0, len(prepared))
for _, prep := range prepared { for _, prep := range prepared {
@@ -1628,6 +1656,12 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
if err := s.attachLatestApproval(ctx, updated); err != nil { if err := s.attachLatestApproval(ctx, updated); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err) s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
} }
s.invalidateDepreciationSnapshots(
ctx,
nil,
collectPFKIDsFromPurchaseItems(itemsToDelete),
resolvePurchaseDepreciationInvalidateDate(purchase, itemsToDelete, time.Now().UTC()),
)
return updated, nil return updated, nil
} }
@@ -1721,6 +1755,12 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
return utils.Internal("Failed to sync expense") return utils.Internal("Failed to sync expense")
} }
} }
s.invalidateDepreciationSnapshots(
ctx,
nil,
collectPFKIDsFromPurchaseItems(itemsToDelete),
resolvePurchaseDepreciationInvalidateDate(purchase, itemsToDelete, time.Now().UTC()),
)
return nil return nil
} }
@@ -2391,7 +2431,17 @@ func (s *purchaseService) rejectAndReload(
if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil { if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil {
return nil, err return nil, err
} }
return s.loadPurchase(c.Context(), purchaseID) updated, err := s.loadPurchase(c.Context(), purchaseID)
if err != nil {
return nil, err
}
s.invalidateDepreciationSnapshots(
c.Context(),
nil,
collectPFKIDsFromPurchase(updated),
resolvePurchaseDepreciationInvalidateDate(updated, updated.Items, time.Now().UTC()),
)
return updated, nil
} }
func (s *purchaseService) loadPurchase( func (s *purchaseService) loadPurchase(
ctx context.Context, ctx context.Context,
@@ -2522,10 +2572,17 @@ func (s *purchaseService) resolveChickinLockedItemIDsByItemID(ctx context.Contex
} }
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint { func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
if p == nil {
return nil
}
return collectPFKIDsFromPurchaseItems(p.Items)
}
func collectPFKIDsFromPurchaseItems(items []entity.PurchaseItem) []uint {
seen := make(map[uint]struct{}) seen := make(map[uint]struct{})
ids := make([]uint, 0) ids := make([]uint, 0)
for _, item := range p.Items { for _, item := range items {
if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 { if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 {
continue continue
} }
@@ -2538,6 +2595,82 @@ func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
} }
return ids return ids
} }
func resolvePurchaseDepreciationInvalidateDate(
purchase *entity.Purchase,
items []entity.PurchaseItem,
fallback time.Time,
) time.Time {
fromDate := time.Time{}
if purchase != nil {
fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, purchase.CreatedAt)
if purchase.PoDate != nil {
fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, *purchase.PoDate)
}
}
for _, item := range items {
if item.ReceivedDate == nil {
continue
}
fromDate = commonSvc.MinNonZeroDateOnlyUTC(fromDate, *item.ReceivedDate)
}
if fromDate.IsZero() {
fromDate = fallback
}
return fromDate
}
func (s *purchaseService) invalidateDepreciationSnapshots(
ctx context.Context,
tx *gorm.DB,
projectFlockKandangIDs []uint,
fromDate time.Time,
) {
if fromDate.IsZero() {
return
}
projectFlockKandangIDs = utils.UniqueUintSlice(projectFlockKandangIDs)
targetDB := s.PurchaseRepo.DB()
if tx != nil {
targetDB = tx
}
var farmIDs []uint
if len(projectFlockKandangIDs) > 0 {
resolvedFarmIDs, err := commonSvc.ResolveProjectFlockIDsByProjectFlockKandangIDs(ctx, targetDB, projectFlockKandangIDs)
if err != nil {
s.Log.Warnf(
"Failed to resolve farm ids for purchase depreciation invalidation (pfk_ids=%v): %+v",
projectFlockKandangIDs,
err,
)
} else {
farmIDs = resolvedFarmIDs
}
}
if len(farmIDs) == 0 {
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, nil, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots globally (from=%s): %+v",
fromDate.Format("2006-01-02"),
err,
)
}
return
}
if err := commonSvc.InvalidateFarmDepreciationSnapshotsFromDate(ctx, targetDB, farmIDs, fromDate); err != nil {
s.Log.Warnf(
"Failed to invalidate depreciation snapshots (farm_ids=%v, from=%s): %+v",
farmIDs,
fromDate.Format("2006-01-02"),
err,
)
}
}
func (s *purchaseService) ensureProjectFlockNotClosedForPurchase( func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
ctx context.Context, ctx context.Context,
purchase *entity.Purchase, purchase *entity.Purchase,
@@ -90,6 +90,75 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
}) })
} }
func (c *RepportController) GetExpenseDepreciation(ctx *fiber.Ctx) error {
rows, meta, err := c.RepportService.GetExpenseDepreciation(ctx)
if err != nil {
return err
}
resp := struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta dto.ExpenseDepreciationMetaDTO `json:"meta"`
Data []dto.ExpenseDepreciationRowDTO `json:"data"`
}{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expense depreciation report successfully",
Meta: *meta,
Data: rows,
}
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) error {
rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx)
if err != nil {
return err
}
resp := struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta dto.ExpenseDepreciationMetaDTO `json:"meta"`
Data []dto.ExpenseDepreciationManualInputRowDTO `json:"data"`
}{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expense depreciation manual inputs successfully",
Meta: *meta,
Data: rows,
}
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx) error {
req := new(validation.ExpenseDepreciationManualInputUpsert)
if err := ctx.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := m.EnsureProjectFlockAccess(ctx, c.RepportService.DB(), req.ProjectFlockID); err != nil {
return err
}
result, err := c.RepportService.UpsertExpenseDepreciationManualInput(ctx, req)
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Upsert expense depreciation manual input successfully",
Data: result,
})
}
func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
query := &validation.MarketingQuery{ query := &validation.MarketingQuery{
Page: ctx.QueryInt("page", 1), Page: ctx.QueryInt("page", 1),
@@ -0,0 +1,43 @@
package dto
type ExpenseDepreciationFiltersDTO struct {
AreaID string `json:"area_id"`
LocationID string `json:"location_id"`
ProjectFlockID string `json:"project_flock_id"`
Period string `json:"period"`
}
type ExpenseDepreciationMetaDTO struct {
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int64 `json:"total_pages"`
TotalResults int64 `json:"total_results"`
Filters ExpenseDepreciationFiltersDTO `json:"filters"`
}
type ExpenseDepreciationRowDTO struct {
ProjectFlockID int64 `json:"project_flock_id"`
FarmName string `json:"farm_name"`
Period string `json:"period"`
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
DepreciationValue float64 `json:"depreciation_value"`
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
Components any `json:"components"`
}
type ExpenseDepreciationManualInputRowDTO struct {
ID int64 `json:"id"`
ProjectFlockID int64 `json:"project_flock_id"`
FarmName string `json:"farm_name"`
TotalCost float64 `json:"total_cost"`
Note *string `json:"note"`
}
func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
return ExpenseDepreciationFiltersDTO{
AreaID: area,
LocationID: location,
ProjectFlockID: projectFlockID,
Period: period,
}
}
+22 -1
View File
@@ -36,6 +36,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
customerRepository := customerRepo.NewCustomerRepository(db) customerRepository := customerRepo.NewCustomerRepository(db)
@@ -45,7 +46,27 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalSvc := approvalService.NewApprovalService(approvalRepository) approvalSvc := approvalService.NewApprovalService(approvalRepository)
hppSvc := approvalService.NewHppService(hppCostRepository) hppSvc := approvalService.NewHppService(hppCostRepository)
repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, hppSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository, standardGrowthDetailRepository, productionStandardDetailRepository) repportService := sRepport.NewRepportService(
db,
validate,
expenseRealizationRepository,
expenseDepreciationRepository,
marketingDeliveryProductRepository,
purchaseRepository,
chickinRepository,
recordingRepository,
approvalSvc,
hppSvc,
hppCostRepository,
purchaseSupplierRepository,
debtSupplierRepository,
hppPerKandangRepository,
productionResultRepository,
customerPaymentRepository,
customerRepository,
standardGrowthDetailRepository,
productionStandardDetailRepository,
)
userService := sUser.NewUserService(userRepository, validate) userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService) RepportRoutes(router, userService, repportService)
@@ -0,0 +1,326 @@
package repositories
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type FarmDepreciationCandidateRow struct {
ProjectFlockID uint
FarmName string
}
type FarmDepreciationLatestTransferRow struct {
ProjectFlockID uint
FarmName string
ProjectFlockKandangID uint
KandangID uint
KandangName string
HouseType *string
SourceProjectFlockID uint
TransferDate time.Time
TransferQty float64
TransferID uint
}
type FarmDepreciationManualInputRow struct {
Id uint
ProjectFlockID uint
FarmName string
TotalCost float64
Note *string
}
type houseDepreciationPercentRow struct {
HouseType string
Day int
DepreciationPercent float64
}
type ExpenseDepreciationRepository interface {
GetCandidateFarms(ctx context.Context, period time.Time, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationCandidateRow, error)
GetSnapshotsByPeriodAndFarmIDs(ctx context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error)
UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error
DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error
GetLatestTransferInputsByFarms(ctx context.Context, period time.Time, farmIDs []uint) ([]FarmDepreciationLatestTransferRow, error)
GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
GetLatestManualInputsByFarms(ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationManualInputRow, error)
UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error
DB() *gorm.DB
}
type expenseDepreciationRepository struct {
db *gorm.DB
}
func NewExpenseDepreciationRepository(db *gorm.DB) ExpenseDepreciationRepository {
return &expenseDepreciationRepository{db: db}
}
func (r *expenseDepreciationRepository) DB() *gorm.DB {
return r.db
}
func (r *expenseDepreciationRepository) GetCandidateFarms(
ctx context.Context,
period time.Time,
areaIDs, locationIDs, projectFlockIDs []int64,
) ([]FarmDepreciationCandidateRow, error) {
rows := make([]FarmDepreciationCandidateRow, 0)
query := r.db.WithContext(ctx).
Table("project_flocks AS pf").
Select("DISTINCT pf.id AS project_flock_id, pf.flock_name AS farm_name").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
Where("pf.deleted_at IS NULL").
Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", period)
if len(areaIDs) > 0 {
query = query.Where("pf.area_id IN ?", areaIDs)
}
if len(locationIDs) > 0 {
query = query.Where("pf.location_id IN ?", locationIDs)
}
if len(projectFlockIDs) > 0 {
query = query.Where("pf.id IN ?", projectFlockIDs)
}
if err := query.Order("pf.id ASC").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *expenseDepreciationRepository) GetSnapshotsByPeriodAndFarmIDs(
ctx context.Context,
period time.Time,
farmIDs []uint,
) ([]entity.FarmDepreciationSnapshot, error) {
if len(farmIDs) == 0 {
return []entity.FarmDepreciationSnapshot{}, nil
}
rows := make([]entity.FarmDepreciationSnapshot, 0)
if err := r.db.WithContext(ctx).
Where("project_flock_id IN ?", farmIDs).
Where("period_date = DATE(?)", period).
Find(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *expenseDepreciationRepository) UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error {
if len(rows) == 0 {
return nil
}
return r.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "project_flock_id"},
{Name: "period_date"},
},
DoUpdates: clause.AssignmentColumns([]string{
"depreciation_percent_effective",
"depreciation_value",
"pullet_cost_day_n_total",
"components",
"updated_at",
}),
}).
Create(&rows).Error
}
func (r *expenseDepreciationRepository) DeleteSnapshotsFromDate(
ctx context.Context,
fromDate time.Time,
farmIDs []uint,
) error {
if fromDate.IsZero() {
return nil
}
query := r.db.WithContext(ctx).
Table("farm_depreciation_snapshots").
Where("period_date >= DATE(?)", fromDate)
if len(farmIDs) > 0 {
query = query.Where("project_flock_id IN ?", farmIDs)
}
return query.Delete(nil).Error
}
func (r *expenseDepreciationRepository) GetLatestTransferInputsByFarms(
ctx context.Context,
period time.Time,
farmIDs []uint,
) ([]FarmDepreciationLatestTransferRow, error) {
if len(farmIDs) == 0 {
return []FarmDepreciationLatestTransferRow{}, nil
}
rows := make([]FarmDepreciationLatestTransferRow, 0)
query := `
WITH latest_transfer_approval AS (
SELECT a.approvable_id, a.action
FROM approvals a
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = @approval_type
GROUP BY approvable_id
) la
ON la.approvable_id = a.approvable_id
AND la.latest_action_at = a.action_at
WHERE a.approvable_type = @approval_type
),
approved_transfers AS (
SELECT
lt.id,
lt.from_project_flock_id,
lt.to_project_flock_id,
COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date
FROM laying_transfers lt
JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id
WHERE lt.deleted_at IS NULL
AND lt.executed_at IS NOT NULL
AND lta.action = 'APPROVED'
)
SELECT DISTINCT ON (ltt.target_project_flock_kandang_id)
pf.id AS project_flock_id,
pf.flock_name AS farm_name,
pfk.id AS project_flock_kandang_id,
k.id AS kandang_id,
k.name AS kandang_name,
k.house_type::text AS house_type,
at.from_project_flock_id AS source_project_flock_id,
at.effective_date AS transfer_date,
ltt.total_qty AS transfer_qty,
at.id AS transfer_id
FROM laying_transfer_targets ltt
JOIN approved_transfers at ON at.id = ltt.laying_transfer_id
JOIN project_flock_kandangs pfk ON pfk.id = ltt.target_project_flock_kandang_id
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE ltt.deleted_at IS NULL
AND pf.id IN @farm_ids
AND at.effective_date <= DATE(@period_date)
ORDER BY ltt.target_project_flock_kandang_id, at.effective_date DESC, at.id DESC
`
if err := r.db.WithContext(ctx).Raw(query, map[string]any{
"approval_type": utils.ApprovalWorkflowTransferToLaying.String(),
"farm_ids": farmIDs,
"period_date": period,
}).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *expenseDepreciationRepository) GetDepreciationPercents(
ctx context.Context,
houseTypes []string,
maxDay int,
) (map[string]map[int]float64, error) {
result := make(map[string]map[int]float64)
if len(houseTypes) == 0 || maxDay <= 0 {
return result, nil
}
rows := make([]houseDepreciationPercentRow, 0)
if err := r.db.WithContext(ctx).
Table("house_depreciation_standards").
Select("house_type::text AS house_type, day, depreciation_percent").
Where("house_type::text IN ?", houseTypes).
Where("day <= ?", maxDay).
Order("house_type ASC, day ASC").
Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if _, exists := result[row.HouseType]; !exists {
result[row.HouseType] = make(map[int]float64)
}
result[row.HouseType][row.Day] = row.DepreciationPercent
}
return result, nil
}
func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms(
ctx context.Context,
areaIDs, locationIDs, projectFlockIDs []int64,
) ([]FarmDepreciationManualInputRow, error) {
rows := make([]FarmDepreciationManualInputRow, 0)
query := r.db.WithContext(ctx).
Table("farm_depreciation_manual_inputs AS fdmi").
Select(`
fdmi.id AS id,
fdmi.project_flock_id AS project_flock_id,
pf.flock_name AS farm_name,
fdmi.total_cost AS total_cost,
fdmi.note AS note
`).
Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id").
Where("pf.deleted_at IS NULL").
Where("pf.category = ?", utils.ProjectFlockCategoryLaying)
if len(areaIDs) > 0 {
query = query.Where("pf.area_id IN ?", areaIDs)
}
if len(locationIDs) > 0 {
query = query.Where("pf.location_id IN ?", locationIDs)
}
if len(projectFlockIDs) > 0 {
query = query.Where("pf.id IN ?", projectFlockIDs)
}
if err := query.
Order("pf.id ASC").
Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error {
if row == nil {
return nil
}
now := time.Now().UTC()
err := r.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "project_flock_id"},
},
DoUpdates: clause.Assignments(map[string]any{
"total_cost": row.TotalCost,
"note": row.Note,
"updated_at": now,
}),
}).
Create(row).Error
if err != nil {
return err
}
return r.db.WithContext(ctx).
Table("farm_depreciation_manual_inputs").
Select("id, project_flock_id, total_cost, note").
Where("project_flock_id = ?", row.ProjectFlockId).
Take(row).Error
}
+3
View File
@@ -16,6 +16,9 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
route.Get("/expense/depreciation", ctrl.GetExpenseDepreciation)
route.Get("/expense/depreciation/manual-inputs", ctrl.GetExpenseDepreciationManualInputs)
route.Put("/expense/depreciation/manual-inputs", ctrl.UpsertExpenseDepreciationManualInput)
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier)
@@ -42,6 +42,9 @@ import (
type RepportService interface { type RepportService interface {
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error)
GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error)
UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error)
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
@@ -56,12 +59,14 @@ type repportService struct {
Validate *validator.Validate Validate *validator.Validate
db *gorm.DB db *gorm.DB
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
ExpenseDepreciationRepo repportRepo.ExpenseDepreciationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService ApprovalSvc approvalService.ApprovalService
HppSvc approvalService.HppService HppSvc approvalService.HppService
HppCostRepo commonRepo.HppCostRepository
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository HppPerKandangRepo repportRepo.HppPerKandangRepository
@@ -85,12 +90,14 @@ func NewRepportService(
db *gorm.DB, db *gorm.DB,
validate *validator.Validate, validate *validator.Validate,
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
expenseDepreciationRepo repportRepo.ExpenseDepreciationRepository,
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
purchaseRepo purchaseRepo.PurchaseRepository, purchaseRepo purchaseRepo.PurchaseRepository,
chickinRepo chickinRepo.ProjectChickinRepository, chickinRepo chickinRepo.ProjectChickinRepository,
recordingRepo recordingRepo.RecordingRepository, recordingRepo recordingRepo.RecordingRepository,
approvalSvc approvalService.ApprovalService, approvalSvc approvalService.ApprovalService,
hppSvc approvalService.HppService, hppSvc approvalService.HppService,
hppCostRepo commonRepo.HppCostRepository,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
debtSupplierRepo repportRepo.DebtSupplierRepository, debtSupplierRepo repportRepo.DebtSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository,
@@ -105,12 +112,14 @@ func NewRepportService(
Validate: validate, Validate: validate,
db: db, db: db,
ExpenseRealizationRepo: expenseRealizationRepo, ExpenseRealizationRepo: expenseRealizationRepo,
ExpenseDepreciationRepo: expenseDepreciationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo, MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo, PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo, RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
HppSvc: hppSvc, HppSvc: hppSvc,
HppCostRepo: hppCostRepo,
PurchaseSupplierRepo: purchaseSupplierRepo, PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo, DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo, HppPerKandangRepo: hppPerKandangRepo,
@@ -164,6 +173,495 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer
return result, total, nil return result, total, nil
} }
func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
params, filters, err := s.parseExpenseDepreciationQuery(ctx)
if err != nil {
return nil, nil, err
}
if err := s.Validate.Struct(params); err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if s.ExpenseDepreciationRepo == nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location)
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
}
candidateRows, err := s.ExpenseDepreciationRepo.GetCandidateFarms(
ctx.Context(),
periodDate,
params.AreaIDs,
params.LocationIDs,
params.ProjectFlockIDs,
)
if err != nil {
return nil, nil, err
}
limit := params.Limit
if limit <= 0 {
limit = 10
}
if len(candidateRows) == 0 {
meta := &dto.ExpenseDepreciationMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: 1,
TotalResults: 0,
Filters: filters,
}
return []dto.ExpenseDepreciationRowDTO{}, meta, nil
}
farmIDs := make([]uint, 0, len(candidateRows))
farmNameByID := make(map[uint]string, len(candidateRows))
for _, row := range candidateRows {
farmIDs = append(farmIDs, row.ProjectFlockID)
farmNameByID[row.ProjectFlockID] = row.FarmName
}
snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs)
if err != nil {
return nil, nil, err
}
snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots))
for _, row := range snapshots {
snapshotByFarmID[row.ProjectFlockId] = row
}
missingFarmIDs := make([]uint, 0)
for _, farmID := range farmIDs {
if _, exists := snapshotByFarmID[farmID]; exists {
continue
}
missingFarmIDs = append(missingFarmIDs, farmID)
}
if len(missingFarmIDs) > 0 {
computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID)
if computeErr != nil {
return nil, nil, computeErr
}
if len(computedSnapshots) > 0 {
if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil {
return nil, nil, err
}
for _, row := range computedSnapshots {
snapshotByFarmID[row.ProjectFlockId] = row
}
}
}
rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows))
for _, candidate := range candidateRows {
snapshot, exists := snapshotByFarmID[candidate.ProjectFlockID]
if !exists {
rows = append(rows, dto.ExpenseDepreciationRowDTO{
ProjectFlockID: int64(candidate.ProjectFlockID),
FarmName: candidate.FarmName,
Period: params.Period,
DepreciationPercentEffective: 0,
DepreciationValue: 0,
PulletCostDayNTotal: 0,
Components: map[string]any{},
})
continue
}
rows = append(rows, dto.ExpenseDepreciationRowDTO{
ProjectFlockID: int64(snapshot.ProjectFlockId),
FarmName: candidate.FarmName,
Period: params.Period,
DepreciationPercentEffective: snapshot.DepreciationPercentEffective,
DepreciationValue: snapshot.DepreciationValue,
PulletCostDayNTotal: snapshot.PulletCostDayNTotal,
Components: parseSnapshotComponents(snapshot.Components),
})
}
totalResults := int64(len(rows))
totalPages := int64(0)
if totalResults > 0 {
totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
}
if totalPages == 0 {
totalPages = 1
}
offset := (params.Page - 1) * limit
if offset < 0 {
offset = 0
}
if offset > len(rows) {
offset = len(rows)
}
end := offset + limit
if end > len(rows) {
end = len(rows)
}
meta := &dto.ExpenseDepreciationMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: totalPages,
TotalResults: totalResults,
Filters: filters,
}
return rows[offset:end], meta, nil
}
func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
params, filters, err := s.parseExpenseDepreciationQuery(ctx)
if err != nil {
return nil, nil, err
}
if s.ExpenseDepreciationRepo == nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
}
repoRows, err := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms(
ctx.Context(),
params.AreaIDs,
params.LocationIDs,
params.ProjectFlockIDs,
)
if err != nil {
return nil, nil, err
}
rows := make([]dto.ExpenseDepreciationManualInputRowDTO, 0, len(repoRows))
for _, row := range repoRows {
rows = append(rows, dto.ExpenseDepreciationManualInputRowDTO{
ID: int64(row.Id),
ProjectFlockID: int64(row.ProjectFlockID),
FarmName: row.FarmName,
TotalCost: row.TotalCost,
Note: row.Note,
})
}
limit := params.Limit
if limit <= 0 {
limit = 10
}
totalResults := int64(len(rows))
totalPages := int64(0)
if totalResults > 0 {
totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
}
if totalPages == 0 {
totalPages = 1
}
offset := (params.Page - 1) * limit
if offset < 0 {
offset = 0
}
if offset > len(rows) {
offset = len(rows)
}
end := offset + limit
if end > len(rows) {
end = len(rows)
}
meta := &dto.ExpenseDepreciationMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: totalPages,
TotalResults: totalResults,
Filters: filters,
}
return rows[offset:end], meta, nil
}
func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) {
if req == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "request is required")
}
if err := s.Validate.Struct(req); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if s.ExpenseDepreciationRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured")
}
row := entity.FarmDepreciationManualInput{
ProjectFlockId: req.ProjectFlockID,
TotalCost: req.TotalCost,
Note: req.Note,
}
if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil {
return nil, err
}
response := &dto.ExpenseDepreciationManualInputRowDTO{
ID: int64(row.Id),
ProjectFlockID: int64(row.ProjectFlockId),
TotalCost: row.TotalCost,
Note: row.Note,
}
listRows, listErr := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms(
ctx.Context(),
nil,
nil,
[]int64{int64(row.ProjectFlockId)},
)
if listErr == nil {
for _, listRow := range listRows {
if listRow.ProjectFlockID == row.ProjectFlockId {
response.FarmName = listRow.FarmName
break
}
}
}
return response, nil
}
type depreciationKandangComponent struct {
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
KandangID uint `json:"kandang_id"`
KandangName string `json:"kandang_name"`
TransferID uint `json:"transfer_id"`
TransferDate string `json:"transfer_date"`
SourceProjectFlockID uint `json:"source_project_flock_id"`
HouseType string `json:"house_type"`
DayN int `json:"day_n"`
DepreciationPercent float64 `json:"depreciation_percent"`
TransferQty float64 `json:"transfer_qty"`
PulletCostDayN float64 `json:"pullet_cost_day_n"`
DepreciationValue float64 `json:"depreciation_value"`
}
type depreciationFarmComponents struct {
KandangCount int `json:"kandang_count"`
Kandang []depreciationKandangComponent `json:"kandang"`
}
func (s *repportService) computeExpenseDepreciationSnapshots(
ctx context.Context,
periodDate time.Time,
farmIDs []uint,
farmNameByID map[uint]string,
) ([]entity.FarmDepreciationSnapshot, error) {
if len(farmIDs) == 0 {
return []entity.FarmDepreciationSnapshot{}, nil
}
inputRows, err := s.ExpenseDepreciationRepo.GetLatestTransferInputsByFarms(ctx, periodDate, farmIDs)
if err != nil {
return nil, err
}
groupedByFarm := make(map[uint][]repportRepo.FarmDepreciationLatestTransferRow, len(farmIDs))
houseTypeSet := make(map[string]struct{})
maxDay := 0
for _, row := range inputRows {
groupedByFarm[row.ProjectFlockID] = append(groupedByFarm[row.ProjectFlockID], row)
dayN := depreciationDayNumber(row.TransferDate, periodDate)
if dayN > maxDay {
maxDay = dayN
}
houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType)))
if houseType != "" {
houseTypeSet[houseType] = struct{}{}
}
}
houseTypes := make([]string, 0, len(houseTypeSet))
for houseType := range houseTypeSet {
houseTypes = append(houseTypes, houseType)
}
sort.Strings(houseTypes)
percentByHouseType, err := s.ExpenseDepreciationRepo.GetDepreciationPercents(ctx, houseTypes, maxDay)
if err != nil {
return nil, err
}
type sourceCostCacheItem struct {
totalDepCost float64
}
sourceCostCache := make(map[string]sourceCostCacheItem)
sourcePopulationCache := make(map[uint]float64)
result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs))
for _, farmID := range farmIDs {
farmRows := groupedByFarm[farmID]
components := depreciationFarmComponents{
KandangCount: len(farmRows),
Kandang: make([]depreciationKandangComponent, 0, len(farmRows)),
}
totalDepreciationValue := 0.0
totalPulletCostDayN := 0.0
for _, row := range farmRows {
dayN := depreciationDayNumber(row.TransferDate, periodDate)
houseType := strings.TrimSpace(strings.ToLower(valueOrEmptyString(row.HouseType)))
transferDateKey := row.TransferDate.Format("2006-01-02")
cacheKey := fmt.Sprintf("%d|%s", row.SourceProjectFlockID, transferDateKey)
cached, exists := sourceCostCache[cacheKey]
if !exists {
endOfDay := row.TransferDate.Add(24 * time.Hour)
sourceDepCost, calcErr := s.HppSvc.GetTotalDepresiasiFlockGrowing(row.SourceProjectFlockID, &endOfDay)
if calcErr != nil {
return nil, calcErr
}
cached = sourceCostCacheItem{totalDepCost: sourceDepCost}
sourceCostCache[cacheKey] = cached
}
sourcePopulation, popExists := sourcePopulationCache[row.SourceProjectFlockID]
if !popExists {
if s.HppCostRepo == nil {
sourcePopulation = 0
} else {
kandangIDs, idsErr := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, row.SourceProjectFlockID)
if idsErr != nil {
return nil, idsErr
}
population, popErr := s.HppCostRepo.GetTotalPopulation(ctx, kandangIDs)
if popErr != nil {
return nil, popErr
}
sourcePopulation = population
}
sourcePopulationCache[row.SourceProjectFlockID] = sourcePopulation
}
initialPulletCost := 0.0
if sourcePopulation > 0 {
initialPulletCost = (cached.totalDepCost * row.TransferQty) / sourcePopulation
}
pulletCostDayN, depreciationValue, depreciationPercent := calculateDepreciationAtDayN(
initialPulletCost,
dayN,
houseType,
percentByHouseType,
)
totalPulletCostDayN += pulletCostDayN
totalDepreciationValue += depreciationValue
components.Kandang = append(components.Kandang, depreciationKandangComponent{
ProjectFlockKandangID: row.ProjectFlockKandangID,
KandangID: row.KandangID,
KandangName: row.KandangName,
TransferID: row.TransferID,
TransferDate: row.TransferDate.Format("2006-01-02"),
SourceProjectFlockID: row.SourceProjectFlockID,
HouseType: houseType,
DayN: dayN,
DepreciationPercent: depreciationPercent,
TransferQty: row.TransferQty,
PulletCostDayN: pulletCostDayN,
DepreciationValue: depreciationValue,
})
}
effectivePercent := 0.0
effectivePercent = calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN)
componentsJSON, marshalErr := json.Marshal(components)
if marshalErr != nil {
return nil, marshalErr
}
result = append(result, entity.FarmDepreciationSnapshot{
ProjectFlockId: farmID,
PeriodDate: periodDate,
DepreciationPercentEffective: effectivePercent,
DepreciationValue: totalDepreciationValue,
PulletCostDayNTotal: totalPulletCostDayN,
Components: componentsJSON,
})
}
return result, nil
}
func depreciationDayNumber(transferDate time.Time, periodDate time.Time) int {
transfer := time.Date(transferDate.Year(), transferDate.Month(), transferDate.Day(), 0, 0, 0, 0, transferDate.Location())
period := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, periodDate.Location())
if period.Before(transfer) {
return 0
}
return int(period.Sub(transfer).Hours()/24) + 1
}
func calculateDepreciationAtDayN(
initialPulletCost float64,
dayN int,
houseType string,
percentByHouseType map[string]map[int]float64,
) (float64, float64, float64) {
if initialPulletCost <= 0 || dayN <= 0 || houseType == "" {
return 0, 0, 0
}
housePercent, exists := percentByHouseType[houseType]
if !exists {
return 0, 0, 0
}
current := initialPulletCost
pulletCostDayN := 0.0
depreciationValue := 0.0
depreciationPercent := 0.0
for day := 1; day <= dayN; day++ {
pct := housePercent[day]
dep := current * (pct / 100)
if day == dayN {
pulletCostDayN = current
depreciationValue = dep
depreciationPercent = pct
}
current -= dep
if current < 0 {
current = 0
}
}
return pulletCostDayN, depreciationValue, depreciationPercent
}
func calculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN float64) float64 {
if totalPulletCostDayN <= 0 {
return 0
}
return (totalDepreciationValue / totalPulletCostDayN) * 100
}
func parseSnapshotComponents(raw []byte) any {
if len(raw) == 0 {
return map[string]any{}
}
var out any
if err := json.Unmarshal(raw, &out); err != nil {
return map[string]any{}
}
return out
}
func valueOrEmptyString(v *string) string {
if v == nil {
return ""
}
return *v
}
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, err return nil, 0, err
@@ -2133,6 +2631,84 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp
return params, filters, nil return params, filters, nil
} }
func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, error) {
page := ctx.QueryInt("page", 1)
if page < 1 {
page = 1
}
limit := ctx.QueryInt("limit", 10)
if limit < 1 {
limit = 10
}
rawArea := ctx.Query("area_id", "")
rawLocation := ctx.Query("location_id", "")
rawProjectFlock := ctx.Query("project_flock_id", "")
period := strings.TrimSpace(ctx.Query("period", ""))
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
if err != nil {
return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
locationIDs, err := parseCommaSeparatedInt64s(rawLocation)
if err != nil {
return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
projectFlockIDs, err := parseCommaSeparatedInt64s(rawProjectFlock)
if err != nil {
return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB())
if err != nil {
return nil, dto.ExpenseDepreciationFiltersDTO{}, err
}
areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB())
if err != nil {
return nil, dto.ExpenseDepreciationFiltersDTO{}, err
}
if locationScope.Restrict {
allowed := toInt64Slice(locationScope.IDs)
if len(allowed) == 0 {
locationIDs = []int64{-1}
} else if len(locationIDs) > 0 {
locationIDs = intersectInt64(locationIDs, allowed)
} else {
locationIDs = allowed
}
}
if areaScope.Restrict {
allowed := toInt64Slice(areaScope.IDs)
if len(allowed) == 0 {
areaIDs = []int64{-1}
} else if len(areaIDs) > 0 {
areaIDs = intersectInt64(areaIDs, allowed)
} else {
areaIDs = allowed
}
}
params := &validation.ExpenseDepreciationQuery{
Page: page,
Limit: limit,
Period: period,
ProjectFlockIDs: projectFlockIDs,
AreaIDs: areaIDs,
LocationIDs: locationIDs,
}
filters := dto.NewExpenseDepreciationFiltersDTO(
rawArea,
rawLocation,
rawProjectFlock,
period,
)
return params, filters, nil
}
func parseCommaSeparatedInt64s(raw string) ([]int64, error) { func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" {
@@ -75,6 +75,21 @@ type HppPerKandangQuery struct {
WeightMax *float64 `query:"-"` WeightMax *float64 `query:"-"`
} }
type ExpenseDepreciationQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=1000,gt=0"`
Period string `query:"period" validate:"required,datetime=2006-01-02"`
ProjectFlockIDs []int64 `query:"-"`
AreaIDs []int64 `query:"-"`
LocationIDs []int64 `query:"-"`
}
type ExpenseDepreciationManualInputUpsert struct {
ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"`
TotalCost float64 `json:"total_cost" validate:"required,gte=0"`
Note *string `json:"note" validate:"omitempty,max=1000"`
}
type ProductionResultQuery struct { type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"` Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
+17
View File
@@ -983,6 +983,23 @@ func describeRoute(route normalizedRoute) routeMeta {
{Name: "area_id", In: "query", Description: "Area id filter.", Example: 1}, {Name: "area_id", In: "query", Description: "Area id filter.", Example: 1},
{Name: "realization_date", In: "query", Description: "Realization date filter (YYYY-MM-DD).", Example: "2026-01-15"}, {Name: "realization_date", In: "query", Description: "Realization date filter (YYYY-MM-DD).", Example: "2026-01-15"},
} }
case "/api/reports/expense/depreciation":
meta.QueryParams = []parameterMeta{
{Name: "page", In: "query", Description: "Page number.", Example: 1},
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
{Name: "period", In: "query", Description: "Daily period filter (YYYY-MM-DD).", Required: true, Example: "2026-01-01"},
{Name: "project_flock_id", In: "query", Description: "Comma separated project flock ids.", Example: "1,2"},
{Name: "area_id", In: "query", Description: "Comma separated area ids.", Example: "1,2"},
{Name: "location_id", In: "query", Description: "Comma separated location ids.", Example: "1,2"},
}
case "/api/reports/expense/depreciation/manual-inputs":
meta.QueryParams = []parameterMeta{
{Name: "page", In: "query", Description: "Page number.", Example: 1},
{Name: "limit", In: "query", Description: "Page size.", Example: 10},
{Name: "project_flock_id", In: "query", Description: "Comma separated project flock ids.", Example: "1,2"},
{Name: "area_id", In: "query", Description: "Comma separated area ids.", Example: "1,2"},
{Name: "location_id", In: "query", Description: "Comma separated location ids.", Example: "1,2"},
}
case "/api/reports/marketing": case "/api/reports/marketing":
meta.QueryParams = []parameterMeta{ meta.QueryParams = []parameterMeta{
{Name: "page", In: "query", Description: "Page number.", Example: 1}, {Name: "page", In: "query", Description: "Page number.", Example: 1},
+11
View File
@@ -581,6 +581,17 @@ const (
KandangStatusActive KandangStatus = "ACTIVE" KandangStatusActive KandangStatus = "ACTIVE"
) )
// -------------------------------------------------------------------
// House Type
// -------------------------------------------------------------------
type HouseType string
const (
HouseTypeOpenHouse HouseType = "open_house"
HouseTypeCloseHouse HouseType = "close_house"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Marketing Type // Marketing Type
// ------------------------------------------------------------------- // -------------------------------------------------------------------
File diff suppressed because it is too large Load Diff