mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
init depresiasi
This commit is contained in:
@@ -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`.",
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+6
@@ -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;
|
||||||
+18
@@ -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;
|
||||||
+4
@@ -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);
|
||||||
|
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_farm_depreciation_manual_inputs_project_flock_id;
|
||||||
|
DROP TABLE IF EXISTS farm_depreciation_manual_inputs;
|
||||||
+16
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user