mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi
This commit is contained in:
@@ -9,6 +9,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
|
||||
github.com/bytedance/sonic v1.12.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/gofiber/contrib/jwt v1.0.10
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
@@ -45,8 +46,10 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
@@ -70,6 +73,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
@@ -94,4 +98,8 @@ require (
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
|
||||
@@ -65,12 +65,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -88,6 +94,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
@@ -184,6 +192,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -344,4 +355,12 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
BEGIN;
|
||||
|
||||
-- Remove grading details from recording_eggs
|
||||
ALTER TABLE recording_eggs
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
DROP COLUMN IF EXISTS weight;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
|
||||
|
||||
-- Restore grading_eggs table for rollback scenarios
|
||||
CREATE TABLE grading_eggs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_egg_id BIGINT NOT NULL,
|
||||
qty NUMERIC(15,3) NOT NULL,
|
||||
grade VARCHAR,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_grading_eggs_recording_egg
|
||||
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_grading_eggs_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_grading_eggs_recording_egg
|
||||
ON grading_eggs (recording_egg_id);
|
||||
|
||||
COMMIT;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
BEGIN;
|
||||
|
||||
-- Remove separate grading table and move grading details into recording_eggs
|
||||
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
|
||||
DROP TABLE IF EXISTS grading_eggs;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||
|
||||
ALTER TABLE recording_eggs
|
||||
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,38 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||
) THEN
|
||||
ALTER TABLE purchase_items
|
||||
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||
) THEN
|
||||
ALTER TABLE purchase_items
|
||||
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
||||
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
||||
|
||||
ALTER TABLE purchase_items
|
||||
DROP COLUMN IF EXISTS expense_nonstock_id,
|
||||
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
||||
ALTER COLUMN vehicle_number DROP NOT NULL,
|
||||
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||
|
||||
ALTER TABLE purchases
|
||||
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
|
||||
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
|
||||
ALTER COLUMN created_at DROP DEFAULT,
|
||||
ALTER COLUMN updated_at DROP DEFAULT;
|
||||
|
||||
ALTER TABLE purchases
|
||||
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE purchases
|
||||
ALTER COLUMN credit_term DROP DEFAULT,
|
||||
ALTER COLUMN grand_total DROP DEFAULT;
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Adjust purchases table to new purchasing schema
|
||||
ALTER TABLE purchases
|
||||
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
|
||||
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
|
||||
ALTER COLUMN created_at SET DEFAULT now(),
|
||||
ALTER COLUMN updated_at SET DEFAULT now();
|
||||
|
||||
ALTER TABLE purchases
|
||||
DROP COLUMN IF EXISTS credit_term,
|
||||
DROP COLUMN IF EXISTS grand_total;
|
||||
|
||||
-- Bring purchase_items in line with new requirements
|
||||
ALTER TABLE purchase_items
|
||||
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||
|
||||
UPDATE purchase_items
|
||||
SET vehicle_number = ''
|
||||
WHERE vehicle_number IS NULL;
|
||||
|
||||
ALTER TABLE purchase_items
|
||||
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
|
||||
ALTER COLUMN vehicle_number SET NOT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_expense_nonstock
|
||||
FOREIGN KEY (expense_nonstock_id)
|
||||
REFERENCES expense_nonstocks(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
||||
ON purchase_items (expense_nonstock_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
||||
ON purchase_items (project_flock_kandang_id);
|
||||
@@ -10,5 +10,6 @@ type ProductWarehouse struct {
|
||||
// Relations
|
||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
|
||||
type ProjectBudget struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockId uint `gorm:"not null"`
|
||||
NonstockId uint `gorm:"not null"`
|
||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
||||
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type ProjectFlock struct {
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ type Purchase struct {
|
||||
PoNumber *string
|
||||
PoDate *time.Time
|
||||
SupplierId uint `gorm:"not null"`
|
||||
CreditTerm *int
|
||||
DueDate *time.Time
|
||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Notes *string
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
@@ -10,6 +10,7 @@ type PurchaseItem struct {
|
||||
ProductId uint `gorm:"not null"`
|
||||
WarehouseId uint `gorm:"not null"`
|
||||
ProductWarehouseId *uint
|
||||
ProjectFlockKandangId *uint
|
||||
ReceivedDate *time.Time
|
||||
TravelNumber *string
|
||||
TravelNumberDocs *string
|
||||
@@ -19,6 +20,7 @@ type PurchaseItem struct {
|
||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
ExpenseNonstockId *uint64
|
||||
|
||||
// Relations
|
||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||
|
||||
@@ -7,24 +7,11 @@ type RecordingEgg struct {
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
Weight *float64 `gorm:"column:weight"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
}
|
||||
|
||||
type GradingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetOne(c, uint(id))
|
||||
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,7 +70,56 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing successfully",
|
||||
Data: dto.ToClosingListDTO(*result),
|
||||
Message: "Retrieved closing information successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Retrieved project information successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing penjualan successfully",
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -26,6 +27,67 @@ type ClosingDetailDTO struct {
|
||||
ClosingListDTO
|
||||
}
|
||||
|
||||
type ClosingSummaryDTO struct {
|
||||
LocationID uint `json:"location_id"`
|
||||
Periode int `json:"periode"`
|
||||
JenisProduk string `json:"jenis_produk"`
|
||||
LabelPopulasi string `json:"label_populasi"`
|
||||
JumlahPopulasi int `json:"jumlah_populasi"`
|
||||
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
|
||||
JenisProject string `json:"jenis_project"`
|
||||
KandangAktif int `json:"kandang_aktif"`
|
||||
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
|
||||
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
|
||||
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||
StatusProject string `json:"status_project"`
|
||||
StatusClosing string `json:"status_closing"`
|
||||
}
|
||||
|
||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||
history := project.KandangHistory
|
||||
|
||||
period := maxPeriod(history)
|
||||
kandangCount := len(history)
|
||||
population := sumPopulation(history)
|
||||
populationInt := int(population)
|
||||
|
||||
return ClosingSummaryDTO{
|
||||
LocationID: project.LocationId,
|
||||
Periode: period,
|
||||
JenisProduk: project.Category,
|
||||
LabelPopulasi: "",
|
||||
JumlahPopulasi: populationInt,
|
||||
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||
JenisProject: "",
|
||||
KandangAktif: kandangCount,
|
||||
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
|
||||
StatusPembayaranPenjualan: "Tempo",
|
||||
StatusPembayaranMitra: "",
|
||||
StatusProject: statusProject,
|
||||
StatusClosing: statusClosing,
|
||||
}
|
||||
}
|
||||
|
||||
func maxPeriod(history []entity.ProjectFlockKandang) int {
|
||||
max := 0
|
||||
for _, h := range history {
|
||||
if h.Period > max {
|
||||
max = h.Period
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
||||
var total float64
|
||||
for _, h := range history {
|
||||
for _, chickin := range h.Chickins {
|
||||
total += chickin.UsageQty + chickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||
)
|
||||
|
||||
// === Response DTO ===
|
||||
|
||||
type SalesDTO struct {
|
||||
Id uint `json:"id"`
|
||||
RealizationDate time.Time `json:"realization_date"`
|
||||
Age int `json:"age"`
|
||||
DoNumber string `json:"do_number"`
|
||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
Weight float64 `json:"weight"`
|
||||
AvgWeight float64 `json:"avg_weight"`
|
||||
Price float64 `json:"price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
PaymentStatus string `json:"payment_status"`
|
||||
}
|
||||
|
||||
type PenjualanRealisasiResponseDTO struct {
|
||||
ProjectType string `json:"project_type"`
|
||||
FlockId uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
|
||||
// todo: usia ayam masih dummy
|
||||
age := 0
|
||||
|
||||
var product *productDTO.ProductRelationDTO
|
||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
|
||||
product = &mapped
|
||||
}
|
||||
|
||||
var customer *customerDTO.CustomerRelationDTO
|
||||
if e.MarketingProduct.Marketing.Customer.Id != 0 {
|
||||
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
|
||||
customer = &mapped
|
||||
}
|
||||
|
||||
var kandang *kandangDTO.KandangRelationDTO
|
||||
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
||||
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
||||
kandang = &mapped
|
||||
}
|
||||
|
||||
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||
|
||||
return SalesDTO{
|
||||
Id: e.Id,
|
||||
RealizationDate: *e.DeliveryDate,
|
||||
Age: age,
|
||||
DoNumber: doNumber,
|
||||
Product: product,
|
||||
Customer: customer,
|
||||
Qty: e.Qty,
|
||||
Weight: e.TotalWeight,
|
||||
AvgWeight: e.AvgWeight,
|
||||
Price: e.UnitPrice,
|
||||
TotalPrice: e.TotalPrice,
|
||||
Kandang: kandang,
|
||||
PaymentStatus: "Paid",
|
||||
}
|
||||
}
|
||||
|
||||
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||
result := make([]SalesDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToSalesDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
period := extractPeriodFromRealisasi(e)
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
ProjectType: projectType,
|
||||
FlockId: projectFlockID,
|
||||
Period: period,
|
||||
Sales: ToSalesDTOs(e),
|
||||
}
|
||||
}
|
||||
|
||||
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
||||
if len(realisasi) > 0 {
|
||||
for _, item := range realisasi {
|
||||
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -5,8 +5,12 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -17,10 +21,14 @@ type ClosingModule struct{}
|
||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
closingRepo := rClosing.NewClosingRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ClosingRoutes(router, userService, closingService)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
||||
ctrl := controller.NewClosingController(s)
|
||||
|
||||
route := v1.Group("/closings")
|
||||
route := v1.Group("/closing")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
@@ -21,5 +21,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -16,20 +23,30 @@ import (
|
||||
|
||||
type ClosingService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||
}
|
||||
|
||||
type closingService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockRepo: projectFlockRepo,
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +54,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser")
|
||||
}
|
||||
|
||||
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db).
|
||||
Preload("KandangHistory").
|
||||
Preload("KandangHistory.Chickins")
|
||||
}
|
||||
|
||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -59,14 +82,109 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
return closings, total, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get closing by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return closing, nil
|
||||
return projectFlock, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
|
||||
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(realisasi) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
||||
}
|
||||
return realisasi, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||
}
|
||||
|
||||
summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing)
|
||||
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||
if s.ApprovalSvc == nil {
|
||||
return "", "Belum Selesai", nil
|
||||
}
|
||||
|
||||
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var (
|
||||
minStep uint16
|
||||
statusProject string
|
||||
completed int
|
||||
)
|
||||
|
||||
for _, rec := range records {
|
||||
if minStep == 0 || rec.StepNumber < minStep {
|
||||
minStep = rec.StepNumber
|
||||
statusProject = rec.StepName
|
||||
}
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
|
||||
if statusProject == "" && minStep > 0 {
|
||||
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok {
|
||||
statusProject = label
|
||||
}
|
||||
}
|
||||
|
||||
statusClosing := "Belum Selesai"
|
||||
switch {
|
||||
case len(records) == 0 || completed == 0:
|
||||
statusClosing = "Belum Selesai"
|
||||
case completed < len(records):
|
||||
statusClosing = "Sebagian"
|
||||
default:
|
||||
statusClosing = "Selesai"
|
||||
}
|
||||
|
||||
return statusProject, statusClosing, nil
|
||||
}
|
||||
|
||||
@@ -151,12 +151,16 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
req.Documents = form.File["documents"]
|
||||
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
|
||||
|
||||
transactionDate := c.FormValue("transaction_date")
|
||||
if transactionDate != "" {
|
||||
req.TransactionDate = &transactionDate
|
||||
}
|
||||
|
||||
categoryVal := c.FormValue("category")
|
||||
if categoryVal != "" {
|
||||
req.Category = &categoryVal
|
||||
}
|
||||
|
||||
supplierIDVal := c.FormValue("supplier_id")
|
||||
if supplierIDVal != "" {
|
||||
@@ -312,13 +316,18 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
||||
|
||||
req.Documents = form.File["documents"]
|
||||
|
||||
req.RealizationDate = c.FormValue("realization_date")
|
||||
realizationDate := c.FormValue("realization_date")
|
||||
if realizationDate != "" {
|
||||
req.RealizationDate = &realizationDate
|
||||
}
|
||||
|
||||
realizationsJSON := c.FormValue("realizations")
|
||||
if realizationsJSON != "" {
|
||||
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
||||
var realizations []validation.RealizationItem
|
||||
if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
||||
}
|
||||
req.Realizations = &realizations
|
||||
}
|
||||
|
||||
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
||||
|
||||
@@ -14,6 +14,8 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
||||
|
||||
route := v1.Group("/expenses")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Use(m.Auth(u))
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
|
||||
@@ -183,7 +183,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
||||
|
||||
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
||||
referenceNumber, err := GenerateExpenseReferenceNumber(c.Context(), expenseRepoTx)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||
}
|
||||
@@ -732,10 +732,10 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||
|
||||
// Check if only updating documents
|
||||
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
|
||||
updateDataOnly := req.Realizations == nil && len(req.Documents) > 0
|
||||
|
||||
if len(req.Realizations) > 0 {
|
||||
for _, realizationItem := range req.Realizations {
|
||||
if req.Realizations != nil {
|
||||
for _, realizationItem := range *req.Realizations {
|
||||
|
||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||
|
||||
@@ -770,6 +770,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
|
||||
}
|
||||
|
||||
if req.RealizationDate != nil {
|
||||
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Documents) > 0 {
|
||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||
return err
|
||||
@@ -1050,17 +1056,6 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) {
|
||||
|
||||
sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
refNum := fmt.Sprintf("BOP-LTI-%05d", sequence)
|
||||
|
||||
return refNum, nil
|
||||
}
|
||||
|
||||
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
||||
|
||||
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
)
|
||||
|
||||
// GenerateExpenseReferenceNumber builds a new reference number using the expense sequence.
|
||||
func GenerateExpenseReferenceNumber(ctx context.Context, repo expenseRepo.ExpenseRepository) (string, error) {
|
||||
sequence, err := repo.GetNextSequence(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("BOP-LTI-%05d", sequence), nil
|
||||
}
|
||||
@@ -29,8 +29,8 @@ type Update struct {
|
||||
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -46,9 +46,9 @@ type CreateRealization struct {
|
||||
}
|
||||
|
||||
type UpdateRealization struct {
|
||||
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
RealizationDate *string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
|
||||
Realizations *[]RealizationItem `form:"realizations" json:"realizations" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
type RealizationItem struct {
|
||||
|
||||
@@ -198,7 +198,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
||||
|
||||
db = s.withRelations(db)
|
||||
|
||||
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
|
||||
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
||||
|
||||
if query.TransactionType != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MarketingDeliveryProductRepository interface {
|
||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
||||
}
|
||||
|
||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
||||
return &MarketingDeliveryProductRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProduct entity.MarketingDeliveryProduct
|
||||
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &deliveryProduct, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
// Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct
|
||||
// Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null)
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Preload("MarketingProduct").
|
||||
Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id").
|
||||
Where("mp.marketing_id = ?", marketingId).
|
||||
Where("marketing_delivery_products.delivery_date IS NOT NULL").
|
||||
Order("marketing_delivery_products.id ASC").
|
||||
Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Warehouse.Location").
|
||||
Preload("Warehouse.Area").
|
||||
Preload("Warehouse.Kandang").
|
||||
Preload("CreatedUser")
|
||||
Preload("ProjectFlockKandang")
|
||||
}
|
||||
|
||||
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
||||
@@ -104,7 +104,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
|
||||
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
||||
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
return db.Order("product_warehouses.id DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -319,15 +319,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
|
||||
})
|
||||
|
||||
for i := range groups {
|
||||
if groups[i].DeliveryDate != nil {
|
||||
dateStr := groups[i].DeliveryDate.Format("20060102")
|
||||
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
|
||||
}
|
||||
groups[i].DoNumber = GenerateDeliveryOrderNumber(soNumber, groups[i].DeliveryDate, groups[i].Warehouse.Id)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
|
||||
dateStr := ""
|
||||
if deliveryDate != nil {
|
||||
dateStr = deliveryDate.Format("20060102")
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
|
||||
}
|
||||
|
||||
func getVehicleNumber(e entity.MarketingProduct) string {
|
||||
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
||||
return e.DeliveryProduct.VehicleNumber
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
@@ -22,7 +21,7 @@ type DeliveryOrdersModule struct{}
|
||||
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
marketingRepo := rMarketing.NewMarketingRepository(db)
|
||||
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package delivery_orderss
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
||||
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -17,6 +17,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.
|
||||
|
||||
// Sisanya di group /delivery-orders
|
||||
route := v1.Group("/delivery-orders")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||
@@ -35,14 +34,14 @@ type deliveryOrdersService struct {
|
||||
Validate *validator.Validate
|
||||
MarketingRepo marketingRepo.MarketingRepository
|
||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository
|
||||
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
}
|
||||
|
||||
func NewDeliveryOrdersService(
|
||||
marketingRepo marketingRepo.MarketingRepository,
|
||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||
marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository,
|
||||
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
) DeliveryOrdersService {
|
||||
@@ -200,7 +199,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
|
||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
||||
@@ -259,7 +258,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
|
||||
}
|
||||
|
||||
|
||||
approvalAction := entity.ApprovalActionApproved
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
@@ -301,7 +299,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
|
||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
+55
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
@@ -8,6 +10,9 @@ import (
|
||||
|
||||
type MarketingDeliveryProductRepository interface {
|
||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductRepositoryImpl struct {
|
||||
@@ -19,3 +24,53 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
// JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas
|
||||
// Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Distinct("marketing_delivery_products.*")
|
||||
|
||||
if callback != nil {
|
||||
db = callback(db)
|
||||
}
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
|
||||
// JOIN untuk filter by marketing_id yang ada di related table
|
||||
db := r.DB().WithContext(ctx).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Where("marketing_products.marketing_id = ?", marketingId)
|
||||
|
||||
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deliveryProducts, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
||||
var deliveryProduct entity.MarketingDeliveryProduct
|
||||
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("marketing_product_id = ?", marketingProductID).
|
||||
First(&deliveryProduct).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &deliveryProduct, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package sales_orders
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
||||
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -14,6 +14,7 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO
|
||||
|
||||
v1.Delete("/:id", ctrl.DeleteOne)
|
||||
route := v1.Group("/sales-orders")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||
@@ -125,7 +124,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
||||
|
||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
|
||||
marketing = &entity.Marketing{
|
||||
@@ -220,7 +219,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
if req.CustomerId != 0 {
|
||||
@@ -527,7 +526,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error {
|
||||
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
|
||||
|
||||
marketingProduct := &entity.MarketingProduct{
|
||||
MarketingId: marketingId,
|
||||
|
||||
@@ -28,6 +28,7 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
AreaId: c.QueryInt("area_id", 0),
|
||||
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -53,11 +53,28 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
db = db.Where("warehouses.name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.AreaId != 0 {
|
||||
db = db.Where("area_id = ?", params.AreaId)
|
||||
}
|
||||
if params.ActiveProjectFlockOnly {
|
||||
db = db.Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM kandangs k
|
||||
JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id
|
||||
JOIN (
|
||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||
ORDER BY approvable_id, action_at DESC
|
||||
) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
|
||||
WHERE k.id = warehouses.kandang_id
|
||||
AND LOWER(latest_approval.step_name) = LOWER(?)
|
||||
)
|
||||
`, "Aktif")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
|
||||
@@ -21,4 +21,5 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||
ActiveProjectFlockOnly bool `query:"active_project_flock"`
|
||||
}
|
||||
|
||||
@@ -302,3 +302,29 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
Message: "Get projectflock kandang successfully",
|
||||
Data: dtoResult})
|
||||
}
|
||||
|
||||
func (u *ProjectflockController) Resubmit(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
req := new(validation.Resubmit)
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.ProjectflockService.Resubmit(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Resubmit projectflock successfully",
|
||||
Data: dto.ToProjectFlockListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||
|
||||
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
@@ -29,6 +30,7 @@ type ProjectFlockListDTO struct {
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -51,6 +53,13 @@ type KandangPeriodSummaryDTO struct {
|
||||
Period int `json:"period"`
|
||||
}
|
||||
|
||||
type ProjectBudgetDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Qty float64 `json:"qty"`
|
||||
Price float64 `json:"price"`
|
||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||
}
|
||||
|
||||
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
|
||||
var createdUser *userDTO.UserRelationDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
@@ -110,6 +119,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
|
||||
ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period),
|
||||
Area: areaSummary,
|
||||
Kandangs: kandangSummaries,
|
||||
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
|
||||
Category: e.Category,
|
||||
Fcr: fcrSummary,
|
||||
Location: locationSummary,
|
||||
@@ -184,3 +194,26 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo
|
||||
FlockName: e.FlockName,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO {
|
||||
var nonstockRef *nonstockDTO.NonstockRelationDTO
|
||||
if e.Nonstock != nil && e.Nonstock.Id != 0 {
|
||||
mapped := nonstockDTO.ToNonstockRelationDTO(*e.Nonstock)
|
||||
nonstockRef = &mapped
|
||||
}
|
||||
|
||||
return ProjectBudgetDTO{
|
||||
Id: e.Id,
|
||||
Qty: e.Qty,
|
||||
Price: e.Price,
|
||||
Nonstock: nonstockRef,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProjectBudgetDTOs(e []entity.ProjectBudget) []ProjectBudgetDTO {
|
||||
result := make([]ProjectBudgetDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToProjectBudgetDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
|
||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||
@@ -27,10 +29,12 @@ type ProjectflockModule struct{}
|
||||
func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
flockRepo := rFlock.NewFlockRepository(db)
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
@@ -39,7 +43,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||
}
|
||||
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
|
||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectflockRoutes(router, userService, projectflockService)
|
||||
|
||||
@@ -54,7 +54,10 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
|
||||
Preload("Location").
|
||||
Preload("Kandangs").
|
||||
Preload("KandangHistory").
|
||||
Preload("KandangHistory.Kandang")
|
||||
Preload("KandangHistory.Kandang").
|
||||
Preload("Budgets").
|
||||
Preload("Budgets.Nonstock").
|
||||
Preload("Budgets.Nonstock.Uom")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||
route.Post("/approvals", ctrl.Approval)
|
||||
route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary)
|
||||
route.Put("/:id/resubmit", ctrl.Resubmit)
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
@@ -38,6 +40,7 @@ type ProjectflockService interface {
|
||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
|
||||
}
|
||||
|
||||
type projectflockService struct {
|
||||
@@ -46,8 +49,10 @@ type projectflockService struct {
|
||||
Repository repository.ProjectflockRepository
|
||||
FlockRepo flockRepository.FlockRepository
|
||||
KandangRepo kandangRepository.KandangRepository
|
||||
NonstockRepo nonstockRepository.NonstockRepository
|
||||
WarehouseRepo warehouseRepository.WarehouseRepository
|
||||
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
|
||||
PivotRepo repository.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
@@ -66,8 +71,11 @@ func NewProjectflockService(
|
||||
pivotRepo repository.ProjectFlockKandangRepository,
|
||||
warehouseRepo warehouseRepository.WarehouseRepository,
|
||||
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
|
||||
nonstockRepo nonstockRepository.NonstockRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
validate *validator.Validate,
|
||||
|
||||
) ProjectflockService {
|
||||
return &projectflockService{
|
||||
Log: utils.Log,
|
||||
@@ -75,6 +83,7 @@ func NewProjectflockService(
|
||||
Repository: repo,
|
||||
FlockRepo: flockRepo,
|
||||
KandangRepo: kandangRepo,
|
||||
NonstockRepo: nonstockRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
PivotRepo: pivotRepo,
|
||||
@@ -298,7 +307,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||
|
||||
// Generate unique flock name (sequential per base name, starting from 1)
|
||||
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -309,7 +317,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return err
|
||||
}
|
||||
|
||||
// Compute period per kandang so every kandang maintains its own cycle history.
|
||||
periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -318,6 +325,10 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionCreated
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
_, err = approvalSvcTx.CreateApproval(
|
||||
@@ -842,3 +853,139 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
|
||||
}
|
||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||
}
|
||||
|
||||
kandangIDs := uniqueUintSlice(req.KandangIds)
|
||||
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang")
|
||||
}
|
||||
if len(kandangs) != len(kandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
|
||||
}
|
||||
|
||||
for _, pb := range req.ProjectBudgets {
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
|
||||
var period int = 1
|
||||
if len(existing.KandangHistory) > 0 {
|
||||
period = existing.KandangHistory[0].Period
|
||||
}
|
||||
|
||||
periods := make(map[uint]int, len(kandangIDs))
|
||||
for _, kandangID := range kandangIDs {
|
||||
periods[kandangID] = period
|
||||
}
|
||||
|
||||
if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionUpdated
|
||||
_, err = approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlock,
|
||||
existing.Id,
|
||||
utils.ProjectFlockStepPengajuan,
|
||||
&action,
|
||||
actorID,
|
||||
nil,
|
||||
)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||
}
|
||||
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock")
|
||||
}
|
||||
|
||||
return s.getOneEntityOnly(c, id)
|
||||
}
|
||||
|
||||
func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error {
|
||||
|
||||
if len(budgets) == 0 {
|
||||
return nil
|
||||
}
|
||||
budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction)
|
||||
|
||||
nonstockMap := make(map[uint]bool)
|
||||
relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets))
|
||||
for _, b := range budgets {
|
||||
if nonstockMap[b.NonstockId] {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId))
|
||||
}
|
||||
nonstockMap[b.NonstockId] = true
|
||||
nonstockID := b.NonstockId
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Nonstock",
|
||||
ID: &nonstockID,
|
||||
Exists: s.NonstockRepo.IdExists,
|
||||
})
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB {
|
||||
return q.Where("project_flock_id = ?", projectFlockID)
|
||||
}); err != nil && err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
records := make([]*entity.ProjectBudget, 0, len(budgets))
|
||||
for _, b := range budgets {
|
||||
records = append(records, &entity.ProjectBudget{
|
||||
ProjectFlockId: projectFlockID,
|
||||
NonstockId: b.NonstockId,
|
||||
Price: b.Price,
|
||||
Qty: b.Qty,
|
||||
})
|
||||
}
|
||||
|
||||
if err := budgetRepo.CreateMany(ctx, records, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ type Create struct {
|
||||
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
|
||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
|
||||
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -27,3 +28,14 @@ type Approve struct {
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
type ProjectBudget struct {
|
||||
NonstockId uint `json:"nonstock_id" validate:"required_strict,number,gt=0"`
|
||||
Price float64 `json:"price" validate:"required_strict,number,gt=0"`
|
||||
Qty float64 `json:"qty" validate:"required_strict,number,gt=0"`
|
||||
}
|
||||
|
||||
type Resubmit struct {
|
||||
KandangIds []uint `json:"kandang_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required_strict,min=1,dive"`
|
||||
}
|
||||
|
||||
@@ -146,27 +146,6 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
|
||||
req := new(validation.SubmitGrading)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.RecordingService.SubmitGrading(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Submit grading eggs successfully",
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) Approve(c *fiber.Ctx) error {
|
||||
req := new(validation.Approve)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -29,9 +28,6 @@ type RecordingRelationDTO struct {
|
||||
FcrValue float64 `json:"fcr_value"`
|
||||
TotalChickQty float64 `json:"total_chick_qty"`
|
||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||
EggGradingStatus *string `json:"egg_grading_status"`
|
||||
EggGradingPendingQty *int `json:"egg_grading_pending_qty"`
|
||||
EggGradingCompletedQty *int `json:"egg_grading_completed_qty"`
|
||||
}
|
||||
|
||||
type RecordingListDTO struct {
|
||||
@@ -72,8 +68,8 @@ type RecordingEggDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Qty int `json:"qty"`
|
||||
Weight *float64 `json:"weight,omitempty"`
|
||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingProductWarehouseDTO struct {
|
||||
@@ -84,11 +80,6 @@ type RecordingProductWarehouseDTO struct {
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
}
|
||||
|
||||
type RecordingEggGradingDTO struct {
|
||||
Grade string `json:"grade,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
@@ -140,8 +131,6 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
latestApproval = snapshot
|
||||
}
|
||||
|
||||
gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
|
||||
|
||||
return RecordingRelationDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||
@@ -156,9 +145,6 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
FcrValue: fcrValue,
|
||||
TotalChickQty: totalChickQty,
|
||||
Approval: latestApproval,
|
||||
EggGradingStatus: gradingStatus,
|
||||
EggGradingPendingQty: gradingPending,
|
||||
EggGradingCompletedQty: gradingCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,29 +239,13 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
||||
Id: egg.Id,
|
||||
ProductWarehouseId: egg.ProductWarehouseId,
|
||||
Qty: egg.Qty,
|
||||
Weight: egg.Weight,
|
||||
ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse),
|
||||
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]RecordingEggGradingDTO, len(gradings))
|
||||
for i, grading := range gradings {
|
||||
result[i] = RecordingEggGradingDTO{
|
||||
Grade: grading.Grade,
|
||||
Qty: grading.Qty,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
||||
if pw == nil {
|
||||
return productWarehouseDTO.ProductWarehouseDTO{}
|
||||
@@ -289,61 +259,6 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro
|
||||
return *mapped
|
||||
}
|
||||
|
||||
const goodEggProductWarehouseID uint = 5
|
||||
|
||||
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
|
||||
goodEggs := filterGoodEggs(e.Eggs)
|
||||
if len(goodEggs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
totalEggs := 0
|
||||
totalGraded := 0.0
|
||||
for _, egg := range goodEggs {
|
||||
totalEggs += egg.Qty
|
||||
for _, grading := range egg.GradingEggs {
|
||||
totalGraded += grading.Qty
|
||||
}
|
||||
}
|
||||
|
||||
if totalEggs == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
pendingFloat := float64(totalEggs) - totalGraded
|
||||
if pendingFloat < 0 {
|
||||
pendingFloat = 0
|
||||
}
|
||||
pendingInt := int(math.Round(pendingFloat))
|
||||
completedInt := int(math.Round(totalGraded))
|
||||
if completedInt < 0 {
|
||||
completedInt = 0
|
||||
}
|
||||
|
||||
if pendingInt > 0 {
|
||||
status := "GRADING_TELUR"
|
||||
return &status, &pendingInt, &completedInt
|
||||
}
|
||||
|
||||
status := "GRADING_SELESAI"
|
||||
zero := 0
|
||||
return &status, &zero, &completedInt
|
||||
}
|
||||
|
||||
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
|
||||
if len(eggs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingEgg, 0, len(eggs))
|
||||
for _, egg := range eggs {
|
||||
if egg.ProductWarehouseId == goodEggProductWarehouseID {
|
||||
result = append(result, egg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
||||
result := approvalDTO.ApprovalRelationDTO{}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
CreatedAt: "id",
|
||||
},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
|
||||
@@ -35,8 +35,6 @@ type RecordingRepository interface {
|
||||
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
||||
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
||||
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
||||
CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error
|
||||
DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error
|
||||
|
||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||
|
||||
@@ -76,8 +74,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Eggs").
|
||||
Preload("Eggs.ProductWarehouse").
|
||||
Preload("Eggs.ProductWarehouse.Product").
|
||||
Preload("Eggs.ProductWarehouse.Warehouse").
|
||||
Preload("Eggs.GradingEggs")
|
||||
Preload("Eggs.ProductWarehouse.Warehouse")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
@@ -188,7 +185,6 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
||||
Preload("Recording.ProjectFlockKandang").
|
||||
Preload("Recording.ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProductWarehouse").
|
||||
Preload("GradingEggs").
|
||||
Where("id = ?", id)
|
||||
|
||||
if err := query.First(&egg).Error; err != nil {
|
||||
@@ -197,17 +193,6 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
||||
return &egg, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&gradings).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error {
|
||||
return tx.Where("recording_egg_id = ?", recordingEggID).Delete(&entity.GradingEgg{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return false, nil
|
||||
|
||||
@@ -18,7 +18,6 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/next-day", ctrl.GetNextDay)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/gradings", ctrl.SubmitGrading)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Post("/approvals", ctrl.Approve)
|
||||
|
||||
@@ -33,7 +33,6 @@ type RecordingService interface {
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
SubmitGrading(ctx *fiber.Ctx, req *validation.SubmitGrading) (*entity.Recording, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
|
||||
}
|
||||
|
||||
@@ -273,7 +272,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionCreated
|
||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepGradingTelur, action, createdRecording.CreatedBy, nil); err != nil {
|
||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create recording approval for %d: %+v", createdRecording.Id, err)
|
||||
return err
|
||||
}
|
||||
@@ -347,16 +346,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
hasExistingGradings := false
|
||||
for _, egg := range recordingEntity.Eggs {
|
||||
if len(egg.GradingEggs) > 0 {
|
||||
hasExistingGradings = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
|
||||
|
||||
if hasBodyChanges {
|
||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||
@@ -441,9 +430,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
hasExistingGradings = false
|
||||
hasEggsAfterUpdate = len(req.Eggs) > 0
|
||||
}
|
||||
|
||||
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
||||
@@ -459,20 +445,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
|
||||
}
|
||||
|
||||
var step approvalutils.ApprovalStep
|
||||
if isLaying {
|
||||
if !hasEggsAfterUpdate {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
} else if hasEggChanges {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
} else if hasExistingGradings {
|
||||
step = utils.RecordingStepPengajuan
|
||||
} else {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
}
|
||||
} else {
|
||||
step = utils.RecordingStepPengajuan
|
||||
}
|
||||
step := utils.RecordingStepPengajuan
|
||||
|
||||
latestApproval := recordingEntity.LatestApproval
|
||||
if latestApproval == nil {
|
||||
@@ -517,109 +490,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s *recordingService) SubmitGrading(c *fiber.Ctx, req *validation.SubmitGrading) (*entity.Recording, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(req.EggsGrading) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "eggs_grading must contain at least one item")
|
||||
}
|
||||
|
||||
recordingEggID := req.EggsGrading[0].RecordingEggId
|
||||
for _, grading := range req.EggsGrading[1:] {
|
||||
if grading.RecordingEggId != recordingEggID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "semua grading harus untuk recording egg yang sama")
|
||||
}
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
var recordingID uint
|
||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
recordingEgg, err := s.Repository.GetRecordingEggByID(ctx, recordingEggID, func(db *gorm.DB) *gorm.DB {
|
||||
return tx
|
||||
})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Recording egg not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get recording egg %d: %+v", recordingEggID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var category string
|
||||
if recordingEgg.Recording.ProjectFlockKandang != nil && recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||
category = strings.ToUpper(recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Category)
|
||||
}
|
||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Grading eggs hanya diperbolehkan pada project flock dengan kategori laying")
|
||||
}
|
||||
|
||||
totalGradingQty := 0.0
|
||||
for _, grading := range req.EggsGrading {
|
||||
totalGradingQty += grading.Qty
|
||||
}
|
||||
|
||||
availableRecorded := float64(recordingEgg.Qty)
|
||||
if totalGradingQty > availableRecorded {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Total grading (%.2f) melebihi jumlah telur tercatat (%.2f)", totalGradingQty, availableRecorded),
|
||||
)
|
||||
}
|
||||
|
||||
if recordingEgg.ProductWarehouse.Id != 0 {
|
||||
availableWarehouse := recordingEgg.ProductWarehouse.Quantity
|
||||
if totalGradingQty > availableWarehouse {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Total grading (%.2f) melebihi stok telur baik (%.2f)", totalGradingQty, availableWarehouse),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.DeleteGradingEggs(tx, recordingEgg.Id); err != nil {
|
||||
s.Log.Errorf("Failed to clear grading eggs for recording egg %d: %+v", recordingEgg.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
gradings := make([]entity.GradingEgg, 0, len(req.EggsGrading))
|
||||
createdBy := recordingEgg.CreatedBy
|
||||
if createdBy == 0 {
|
||||
createdBy = recordingEgg.Recording.CreatedBy
|
||||
}
|
||||
for _, item := range req.EggsGrading {
|
||||
gradings = append(gradings, entity.GradingEgg{
|
||||
RecordingEggId: recordingEgg.Id,
|
||||
Grade: strings.TrimSpace(item.Grade),
|
||||
Qty: item.Qty,
|
||||
CreatedBy: createdBy,
|
||||
})
|
||||
}
|
||||
|
||||
if len(gradings) > 0 {
|
||||
if err := s.Repository.CreateGradingEggs(tx, gradings); err != nil {
|
||||
s.Log.Errorf("Failed to persist grading eggs for recording egg %d: %+v", recordingEgg.Id, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionUpdated
|
||||
if err := s.createRecordingApproval(ctx, tx, recordingEgg.RecordingId, utils.RecordingStepPengajuan, action, createdBy, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create approval after grading for recording %d: %+v", recordingEgg.RecordingId, err)
|
||||
return err
|
||||
}
|
||||
|
||||
recordingID = recordingEgg.RecordingId
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
return nil, transactionErr
|
||||
}
|
||||
|
||||
return s.GetOne(c, recordingID)
|
||||
}
|
||||
|
||||
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
@@ -934,14 +804,10 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||
}
|
||||
|
||||
fcrId, err := s.Repository.GetFcrID(tx, recording.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getFcrID: %w", err)
|
||||
}
|
||||
|
||||
currentAvgGrams := recordingutil.ToGrams(currentAvgWeight)
|
||||
currentAvgKg := recordingutil.GramsToKg(currentAvgGrams)
|
||||
prevAvgGrams := recordingutil.ToGrams(prevAvgWeight)
|
||||
prevAvgKg := recordingutil.GramsToKg(prevAvgGrams)
|
||||
|
||||
currentDepletion := float64(totalDepletionQty)
|
||||
cumDepletionQty := prevCumDepletionQty + currentDepletion
|
||||
@@ -951,9 +817,10 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
}
|
||||
recording.TotalDepletionQty = &cumDepletionQty
|
||||
|
||||
var remainingChick float64
|
||||
if totalChick > 0 {
|
||||
totalChickFloat := float64(totalChick)
|
||||
remainingChick := totalChickFloat - cumDepletionQty
|
||||
remainingChick = totalChickFloat - cumDepletionQty
|
||||
if remainingChick < 0 {
|
||||
remainingChick = 0
|
||||
}
|
||||
@@ -978,24 +845,19 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
updates["daily_gain"] = dailyGainKg
|
||||
recording.DailyGain = &dailyGainKg
|
||||
} else {
|
||||
updates["daily_gain"] = gorm.Expr("NULL")
|
||||
recording.DailyGain = nil
|
||||
dailyGainKg := 0.0
|
||||
updates["daily_gain"] = dailyGainKg
|
||||
recording.DailyGain = &dailyGainKg
|
||||
}
|
||||
|
||||
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
||||
if fcrWeightKg, ok, err := s.Repository.GetFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
||||
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
||||
} else if ok {
|
||||
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
||||
if currentAvgKg > 0 && remainingChick > 0 {
|
||||
avgDailyGain := (currentAvgKg - prevAvgKg) / remainingChick
|
||||
updates["avg_daily_gain"] = avgDailyGain
|
||||
recording.AvgDailyGain = &avgDailyGain
|
||||
} else {
|
||||
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
||||
recording.AvgDailyGain = nil
|
||||
}
|
||||
} else {
|
||||
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
||||
recording.AvgDailyGain = nil
|
||||
avgDailyGain := 0.0
|
||||
updates["avg_daily_gain"] = avgDailyGain
|
||||
recording.AvgDailyGain = &avgDailyGain
|
||||
}
|
||||
|
||||
if usageInGrams > 0 && totalChick > 0 {
|
||||
|
||||
@@ -21,6 +21,7 @@ type (
|
||||
Egg struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty int `json:"qty" validate:"required,number,min=0"`
|
||||
Weight *float64 `json:"weight,omitempty" validate:"omitempty,gte=0"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -45,16 +46,6 @@ type Query struct {
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
}
|
||||
|
||||
type EggGrading struct {
|
||||
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
|
||||
Grade string `json:"grade" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
type SubmitGrading struct {
|
||||
EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
Action string `json:"action" validate:"required_strict"`
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
|
||||
@@ -21,13 +21,10 @@ type PurchaseRelationDTO struct {
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
|
||||
type PurchaseListDTO struct {
|
||||
PurchaseRelationDTO
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
CreditTerm *int `json:"credit_term"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
GrandTotal float64 `json:"grand_total"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -37,9 +34,7 @@ type PurchaseListDTO struct {
|
||||
type PurchaseDetailDTO struct {
|
||||
PurchaseRelationDTO
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
CreditTerm *int `json:"credit_term"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
GrandTotal float64 `json:"grand_total"`
|
||||
Items []PurchaseItemDTO `json:"items"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -145,9 +140,7 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
return PurchaseListDTO{
|
||||
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
||||
Supplier: supplier,
|
||||
CreditTerm: p.CreditTerm,
|
||||
DueDate: p.DueDate,
|
||||
GrandTotal: p.GrandTotal,
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
@@ -188,9 +181,7 @@ func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
||||
return PurchaseDetailDTO{
|
||||
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
||||
Supplier: supplier,
|
||||
CreditTerm: p.CreditTerm,
|
||||
DueDate: p.DueDate,
|
||||
GrandTotal: p.GrandTotal,
|
||||
Items: ToPurchaseItemDTOs(p.Items),
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: p.CreatedAt,
|
||||
|
||||
@@ -8,15 +8,20 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
expenseService "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -28,13 +33,49 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||
}
|
||||
expenseBridge := service.NewNoopPurchaseExpenseBridge()
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||
}
|
||||
expenseServiceInstance := expenseService.NewExpenseService(
|
||||
expenseRepository,
|
||||
supplierRepo,
|
||||
nonstockRepo,
|
||||
approvalService,
|
||||
expenseRealizationRepo,
|
||||
projectFlockKandangRepository,
|
||||
validate,
|
||||
)
|
||||
expenseBridge := service.NewExpenseBridge(
|
||||
db,
|
||||
purchaseRepo,
|
||||
projectFlockKandangRepository,
|
||||
expenseServiceInstance,
|
||||
)
|
||||
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("PURCHASE_ITEMS"),
|
||||
Table: "purchase_items",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "id",
|
||||
},
|
||||
OrderBy: []string{"id ASC"},
|
||||
})
|
||||
|
||||
purchaseService := service.NewPurchaseService(
|
||||
validate,
|
||||
@@ -43,8 +84,10 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
warehouseRepo,
|
||||
supplierRepo,
|
||||
productWarehouseRepo,
|
||||
projectFlockKandangRepository,
|
||||
approvalService,
|
||||
expenseBridge,
|
||||
fifoService,
|
||||
)
|
||||
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
@@ -19,12 +19,12 @@ type PurchaseRepository interface {
|
||||
repository.BaseRepository[entity.Purchase]
|
||||
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
|
||||
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate) error
|
||||
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
|
||||
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
||||
UpdateGrandTotal(ctx context.Context, purchaseID uint, grandTotal float64) error
|
||||
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
|
||||
}
|
||||
|
||||
type PurchaseRepositoryImpl struct {
|
||||
@@ -59,6 +59,34 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error {
|
||||
if purchaseID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := `
|
||||
WITH latest_pfk AS (
|
||||
SELECT pfk.id, pfk.kandang_id
|
||||
FROM project_flock_kandangs pfk
|
||||
JOIN (
|
||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||
ORDER BY approvable_id, action_at DESC
|
||||
) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
|
||||
WHERE LOWER(latest_approval.step_name) = LOWER('Aktif')
|
||||
)
|
||||
UPDATE purchase_items pi
|
||||
SET project_flock_kandang_id = lp.id
|
||||
FROM warehouses w
|
||||
JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id
|
||||
WHERE pi.purchase_id = ?
|
||||
AND pi.project_flock_kandang_id IS NULL
|
||||
AND pi.warehouse_id = w.id;
|
||||
`
|
||||
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
@@ -99,7 +127,6 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
ctx context.Context,
|
||||
purchaseID uint,
|
||||
updates []PurchasePricingUpdate,
|
||||
grandTotal float64,
|
||||
) error {
|
||||
if len(updates) == 0 {
|
||||
return errors.New("pricing updates cannot be empty")
|
||||
@@ -133,14 +160,6 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchaseID).
|
||||
Updates(map[string]interface{}{
|
||||
"grand_total": grandTotal,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -201,20 +220,6 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
|
||||
ctx context.Context,
|
||||
purchaseID uint,
|
||||
grandTotal float64,
|
||||
) error {
|
||||
return r.DB().WithContext(ctx).
|
||||
Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchaseID).
|
||||
Updates(map[string]interface{}{
|
||||
"grand_total": grandTotal,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
|
||||
if len(itemIDs) == 0 {
|
||||
return errors.New("itemIDs cannot be empty")
|
||||
|
||||
@@ -2,42 +2,757 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||
expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||
expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
|
||||
type PurchaseExpenseBridge interface {
|
||||
OnItemsCreated(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
|
||||
OnItemsDeleted(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
||||
OnItemsReceived(ctx context.Context, purchaseID uint, updates []ExpenseReceivingPayload) error
|
||||
OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
|
||||
OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error
|
||||
}
|
||||
|
||||
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
|
||||
type ExpenseReceivingPayload struct {
|
||||
PurchaseItemID uint
|
||||
ProductID uint
|
||||
WarehouseID uint
|
||||
SupplierID uint
|
||||
TransportPerItem *float64
|
||||
ReceivedQty float64
|
||||
ReceivedDate *time.Time
|
||||
}
|
||||
|
||||
// noopPurchaseExpenseBridge is the default implementation until the expense module is ready.
|
||||
type noopPurchaseExpenseBridge struct{}
|
||||
|
||||
func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
|
||||
return &noopPurchaseExpenseBridge{}
|
||||
type groupedItem struct {
|
||||
item *entity.PurchaseItem
|
||||
payload ExpenseReceivingPayload
|
||||
projectFK *uint
|
||||
kandangID *uint
|
||||
totalPrice float64
|
||||
}
|
||||
|
||||
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint, _ []entity.PurchaseItem) error {
|
||||
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
||||
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
||||
}
|
||||
|
||||
type expenseBridge struct {
|
||||
db *gorm.DB
|
||||
purchaseRepo rPurchase.PurchaseRepository
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
expenseSvc expenseSvc.ExpenseService
|
||||
}
|
||||
|
||||
func NewExpenseBridge(
|
||||
db *gorm.DB,
|
||||
purchaseRepo rPurchase.PurchaseRepository,
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||
expenseSvc expenseSvc.ExpenseService,
|
||||
) PurchaseExpenseBridge {
|
||||
return &expenseBridge{
|
||||
db: db,
|
||||
purchaseRepo: purchaseRepo,
|
||||
projectFlockKandangRepo: projectFlockKandangRepo,
|
||||
expenseSvc: expenseSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []entity.PurchaseItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
expenseIDs := make(map[uint64]struct{})
|
||||
expenseNonstockIDs := make([]uint64, 0)
|
||||
for _, item := range items {
|
||||
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
||||
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
||||
}
|
||||
}
|
||||
if len(expenseNonstockIDs) > 0 {
|
||||
for _, nsID := range expenseNonstockIDs {
|
||||
var expenseID uint64
|
||||
if err := tx.
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Select("expense_id").
|
||||
Where("id = ?", nsID).
|
||||
Scan(&expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if expenseID != 0 {
|
||||
expenseIDs[expenseID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var links []struct {
|
||||
ItemID uint
|
||||
ExpenseNonstockID *uint64
|
||||
}
|
||||
if err := tx.
|
||||
Model(&entity.PurchaseItem{}).
|
||||
Select("id as item_id, expense_nonstock_id").
|
||||
Where("id IN ?", extractIDs(items)).
|
||||
Scan(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, link := range links {
|
||||
if link.ExpenseNonstockID == nil || *link.ExpenseNonstockID == 0 {
|
||||
continue
|
||||
}
|
||||
var expenseID uint64
|
||||
if err := tx.
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Select("expense_id").
|
||||
Where("id = ?", *link.ExpenseNonstockID).
|
||||
Scan(&expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if expenseID != 0 {
|
||||
expenseIDs[expenseID] = struct{}{}
|
||||
}
|
||||
if err := tx.Delete(&entity.ExpenseNonstock{}, *link.ExpenseNonstockID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for expenseID := range expenseIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", expenseID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
itemIDs := make([]uint, 0, len(updates))
|
||||
for _, upd := range updates {
|
||||
if upd.PurchaseItemID != 0 {
|
||||
itemIDs = append(itemIDs, upd.PurchaseItemID)
|
||||
}
|
||||
}
|
||||
if len(itemIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var links []struct {
|
||||
ItemID uint
|
||||
ExpenseNonstockID *uint64
|
||||
}
|
||||
if err := tx.Model(&entity.PurchaseItem{}).
|
||||
Select("id as item_id, expense_nonstock_id").
|
||||
Where("id IN ?", itemIDs).
|
||||
Scan(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expenseIDs := make(map[uint64]struct{})
|
||||
expenseNonstockIDs := make([]uint64, 0)
|
||||
for _, link := range links {
|
||||
if link.ExpenseNonstockID != nil && *link.ExpenseNonstockID != 0 {
|
||||
expenseNonstockIDs = append(expenseNonstockIDs, *link.ExpenseNonstockID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expenseNonstockIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, nsID := range expenseNonstockIDs {
|
||||
var expenseID uint64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Select("expense_id").
|
||||
Where("id = ?", nsID).
|
||||
Scan(&expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if expenseID != 0 {
|
||||
expenseIDs[expenseID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for expenseID := range expenseIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", expenseID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error {
|
||||
if len(expenseIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for _, id := range expenseIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", id).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&entity.Expense{}, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
||||
if len(expenseIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
action := entity.ApprovalActionUpdated
|
||||
for id := range expenseIDs {
|
||||
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint, _ []uint) error {
|
||||
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
|
||||
if purchaseID == 0 || len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
// Load current links to decide whether to update in place or recreate.
|
||||
type itemLink struct {
|
||||
ExpenseNonstockID uint64
|
||||
ExpenseID uint64
|
||||
SupplierID uint
|
||||
TransactionDate time.Time
|
||||
Qty float64
|
||||
Price float64
|
||||
}
|
||||
|
||||
purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Items").
|
||||
Preload("Items.Warehouse").
|
||||
Preload("Items.Warehouse.Kandang")
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
itemLinks := make(map[uint]itemLink)
|
||||
updatedExpenses := make(map[uint64]struct{})
|
||||
if len(updates) > 0 {
|
||||
ids := make([]uint, 0, len(updates))
|
||||
for _, upd := range updates {
|
||||
if upd.PurchaseItemID != 0 {
|
||||
ids = append(ids, upd.PurchaseItemID)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
rows := make([]struct {
|
||||
ItemID uint
|
||||
ExpenseNonstockID uint64
|
||||
ExpenseID uint64
|
||||
SupplierID uint
|
||||
TransactionDate time.Time
|
||||
Qty float64
|
||||
Price float64
|
||||
}, 0)
|
||||
if err := b.db.WithContext(ctx).
|
||||
Table("purchase_items pi").
|
||||
Select("pi.id as item_id, en.id as expense_nonstock_id, en.expense_id, e.supplier_id, e.transaction_date, en.qty, en.price").
|
||||
Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
|
||||
Joins("LEFT JOIN expenses e ON e.id = en.expense_id").
|
||||
Where("pi.id IN ?", ids).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Build quick lookup per item and per group key for existing expenses.
|
||||
for _, row := range rows {
|
||||
itemLinks[row.ItemID] = itemLink{
|
||||
ExpenseNonstockID: row.ExpenseNonstockID,
|
||||
ExpenseID: row.ExpenseID,
|
||||
SupplierID: row.SupplierID,
|
||||
TransactionDate: row.TransactionDate,
|
||||
Qty: row.Qty,
|
||||
Price: row.Price,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
itemMap[purchase.Items[i].Id] = &purchase.Items[i]
|
||||
}
|
||||
|
||||
groups := make(map[string][]groupedItem)
|
||||
|
||||
for _, payload := range updates {
|
||||
if payload.ReceivedDate == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
||||
}
|
||||
item := itemMap[payload.PurchaseItemID]
|
||||
if item == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||
}
|
||||
if payload.ReceivedQty <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d must be greater than 0", payload.PurchaseItemID))
|
||||
}
|
||||
|
||||
receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
|
||||
supplierID := payload.SupplierID
|
||||
if supplierID == 0 {
|
||||
supplierID = purchase.SupplierId
|
||||
}
|
||||
|
||||
// Decide whether to update existing expense_nonstock or create new.
|
||||
link, hasLink := itemLinks[payload.PurchaseItemID]
|
||||
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
||||
newDate := receivedDate
|
||||
oldSupplier := link.SupplierID
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
|
||||
// If supplier/date unchanged, update nonstock in place.
|
||||
if oldSupplier == supplierID && oldDate.Equal(newDate) {
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(map[string]interface{}{
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if link.ExpenseID != 0 {
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Supplier/date changed: if the linked expense has only this nonstock, update it in place.
|
||||
if link.ExpenseID != 0 {
|
||||
var cnt int64
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", link.ExpenseID).
|
||||
Count(&cnt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt == 1 {
|
||||
if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
||||
}
|
||||
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.Expense{}).
|
||||
Where("id = ?", link.ExpenseID).
|
||||
Updates(map[string]interface{}{
|
||||
"transaction_date": newDate,
|
||||
"supplier_id": supplierID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updateBody := map[string]interface{}{
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
"nonstock_id": newNonstockID,
|
||||
"kandang_id": uint64(*item.Warehouse.KandangId),
|
||||
}
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(updateBody).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
// Expense has multiple nonstocks: create new expense header for this item, then move existing nonstock to it.
|
||||
var kandangID *uint
|
||||
var projectFK *uint
|
||||
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||
id := uint(*item.Warehouse.KandangId)
|
||||
kandangID = &id
|
||||
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||
pid := uint(project.Id)
|
||||
projectFK = &pid
|
||||
}
|
||||
}
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
totalPrice := pricePerItem * payload.ReceivedQty
|
||||
|
||||
gItem := groupedItem{
|
||||
item: item,
|
||||
payload: payload,
|
||||
projectFK: projectFK,
|
||||
kandangID: kandangID,
|
||||
totalPrice: totalPrice,
|
||||
}
|
||||
|
||||
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expenseDetail, err := b.createExpenseViaService(c, purchase, []groupedItem{gItem}, newDate, newNonstockID, purchase.PoNumber, supplierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var createdNonstockID uint64
|
||||
if expenseDetail != nil {
|
||||
noteMap := mapExpenseNotes(expenseDetail)
|
||||
createdNonstockID = noteMap[payload.PurchaseItemID]
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||
updateBody := map[string]interface{}{
|
||||
"expense_id": expenseDetail.Id,
|
||||
"qty": payload.ReceivedQty,
|
||||
"price": pricePerItem,
|
||||
"notes": note,
|
||||
"nonstock_id": newNonstockID,
|
||||
}
|
||||
if kandangID != nil {
|
||||
updateBody["kandang_id"] = uint64(*kandangID)
|
||||
}
|
||||
if projectFK != nil {
|
||||
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
|
||||
}
|
||||
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.ExpenseNonstock{}).
|
||||
Where("id = ?", link.ExpenseNonstockID).
|
||||
Updates(updateBody).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createdNonstockID != 0 {
|
||||
if err := b.db.WithContext(ctx).Delete(&entity.ExpenseNonstock{}, createdNonstockID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if link.ExpenseID != 0 {
|
||||
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||
}
|
||||
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise create new expense/nonstock in grouping flow.
|
||||
}
|
||||
|
||||
baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||
key := baseKey
|
||||
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||
key = fmt.Sprintf("%s:%d", baseKey, payload.PurchaseItemID)
|
||||
}
|
||||
|
||||
var kandangID *uint
|
||||
var projectFK *uint
|
||||
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||
id := uint(*item.Warehouse.KandangId)
|
||||
kandangID = &id
|
||||
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||
pid := uint(project.Id)
|
||||
projectFK = &pid
|
||||
}
|
||||
}
|
||||
|
||||
pricePerItem := item.Price
|
||||
if payload.TransportPerItem != nil {
|
||||
pricePerItem = *payload.TransportPerItem
|
||||
}
|
||||
totalPrice := pricePerItem * payload.ReceivedQty
|
||||
|
||||
groups[key] = append(groups[key], groupedItem{
|
||||
item: item,
|
||||
payload: payload,
|
||||
projectFK: projectFK,
|
||||
kandangID: kandangID,
|
||||
totalPrice: totalPrice,
|
||||
})
|
||||
}
|
||||
|
||||
for key, items := range groups {
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) < 3 {
|
||||
return errors.New("invalid expense grouping key")
|
||||
}
|
||||
expenseDate, err := utils.ParseDateString(parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
supplierID, err := strconv.ParseUint(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expenseDetail, err := b.createExpenseViaService(c, purchase, items, expenseDate, expeditionNonstockID, purchase.PoNumber, uint(supplierID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil {
|
||||
return err
|
||||
}
|
||||
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(updatedExpenses) > 0 {
|
||||
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint, _ []ExpenseReceivingPayload) error {
|
||||
func (b *expenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
|
||||
var id uint64
|
||||
err := b.db.WithContext(ctx).
|
||||
Table("nonstocks AS ns").
|
||||
Select("ns.id").
|
||||
Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
|
||||
Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
|
||||
Where("nss.supplier_id = ?", supplierID).
|
||||
Order("ns.id").
|
||||
Limit(1).
|
||||
Scan(&id).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if id == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func extractIDs(items []entity.PurchaseItem) []uint {
|
||||
result := make([]uint, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Id != 0 {
|
||||
result = append(result, item.Id)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *expenseBridge) createExpenseViaService(
|
||||
c *fiber.Ctx,
|
||||
purchase *entity.Purchase,
|
||||
items []groupedItem,
|
||||
expenseDate time.Time,
|
||||
expeditionNonstockID uint64,
|
||||
poNumber *string,
|
||||
supplierID uint,
|
||||
) (*expenseDto.ExpenseDetailDTO, error) {
|
||||
ctx := c.Context()
|
||||
if b.expenseSvc == nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
|
||||
}
|
||||
|
||||
kandangID := items[0].kandangID
|
||||
if kandangID == nil || *kandangID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
||||
}
|
||||
|
||||
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
||||
for _, gi := range items {
|
||||
note := fmt.Sprintf("purchase_item:%d", gi.payload.PurchaseItemID)
|
||||
price := gi.item.Price
|
||||
if gi.payload.TransportPerItem != nil {
|
||||
price = *gi.payload.TransportPerItem
|
||||
}
|
||||
costItems = append(costItems, expenseValidation.CostItem{
|
||||
NonstockID: expeditionNonstockID,
|
||||
Quantity: gi.payload.ReceivedQty,
|
||||
Price: price,
|
||||
Notes: note,
|
||||
})
|
||||
}
|
||||
|
||||
req := &expenseValidation.Create{
|
||||
PoNumber: "",
|
||||
TransactionDate: utils.FormatDate(expenseDate),
|
||||
Category: "BOP",
|
||||
SupplierID: uint64(supplierID),
|
||||
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
||||
KandangID: uint64(*kandangID),
|
||||
CostItems: costItems,
|
||||
}},
|
||||
}
|
||||
if poNumber != nil {
|
||||
req.PoNumber = *poNumber
|
||||
}
|
||||
|
||||
detail, err := b.expenseSvc.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark approvals up to Finance so latest is Manager Finance
|
||||
action := entity.ApprovalActionApproved
|
||||
actorID := uint(purchase.CreatedBy)
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedItem) error {
|
||||
if detail == nil || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
noteToExpenseNonstock := mapExpenseNotes(detail)
|
||||
|
||||
if len(noteToExpenseNonstock) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, gi := range items {
|
||||
expenseNonstockID, ok := noteToExpenseNonstock[gi.payload.PurchaseItemID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := b.db.WithContext(ctx).
|
||||
Model(&entity.PurchaseItem{}).
|
||||
Where("id = ?", gi.payload.PurchaseItemID).
|
||||
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
|
||||
result := make(map[uint]uint64)
|
||||
if detail == nil {
|
||||
return result
|
||||
}
|
||||
for _, kandang := range detail.Kandangs {
|
||||
for _, pengajuan := range kandang.Pengajuans {
|
||||
note := strings.TrimSpace(pengajuan.Notes)
|
||||
if note == "" {
|
||||
continue
|
||||
}
|
||||
const prefix = "purchase_item:"
|
||||
if !strings.HasPrefix(note, prefix) {
|
||||
continue
|
||||
}
|
||||
idStr := strings.TrimPrefix(note, prefix)
|
||||
var itemID uint
|
||||
if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
|
||||
continue
|
||||
}
|
||||
result[itemID] = pengajuan.Id
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -40,6 +42,7 @@ type PurchaseService interface {
|
||||
|
||||
const (
|
||||
priceTolerance = 0.0001
|
||||
purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS")
|
||||
)
|
||||
|
||||
type purchaseService struct {
|
||||
@@ -50,15 +53,16 @@ type purchaseService struct {
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseBridge PurchaseExpenseBridge
|
||||
FifoSvc commonSvc.FifoService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
|
||||
type staffAdjustmentPayload struct {
|
||||
PricingUpdates []rPurchase.PurchasePricingUpdate
|
||||
NewItems []*entity.PurchaseItem
|
||||
GrandTotal float64
|
||||
}
|
||||
|
||||
func NewPurchaseService(
|
||||
@@ -68,12 +72,11 @@ func NewPurchaseService(
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
supplierRepo rSupplier.SupplierRepository,
|
||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
expenseBridge PurchaseExpenseBridge,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
) PurchaseService {
|
||||
if expenseBridge == nil {
|
||||
expenseBridge = NewNoopPurchaseExpenseBridge()
|
||||
}
|
||||
return &purchaseService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -82,8 +85,10 @@ func NewPurchaseService(
|
||||
WarehouseRepo: warehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseBridge: expenseBridge,
|
||||
FifoSvc: fifoSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||
}
|
||||
}
|
||||
@@ -114,9 +119,9 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
createdFrom, createdTo, err := parseQueryDates(params.CreatedFrom, params.CreatedTo)
|
||||
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
@@ -225,6 +230,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
productId uint
|
||||
warehouseId uint
|
||||
subQty float64
|
||||
pfkID *uint
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
@@ -233,31 +239,47 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
|
||||
warehouseCache := make(map[uint]*entity.Warehouse)
|
||||
productSupplierCache := make(map[uint]bool)
|
||||
getWarehouse := func(id uint) (*entity.Warehouse, error) {
|
||||
getWarehouse := func(id uint) (*entity.Warehouse, *uint, error) {
|
||||
if warehouse, ok := warehouseCache[id]; ok {
|
||||
return warehouse, nil
|
||||
return warehouse, nil, nil
|
||||
}
|
||||
warehouse, err := s.WarehouseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Area").Preload("location")
|
||||
return db.Preload("Area").Preload("Location")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
||||
return nil, nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
}
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
||||
}
|
||||
var pfkID *uint
|
||||
if s.ProjectFlockKandangRepo != nil {
|
||||
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
||||
idCopy := uint(pfk.Id)
|
||||
pfkID = &idCopy
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d has no active project flock", id))
|
||||
} else if err != nil {
|
||||
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||
}
|
||||
}
|
||||
|
||||
warehouseCache[id] = warehouse
|
||||
return warehouse, nil
|
||||
return warehouse, pfkID, nil
|
||||
}
|
||||
|
||||
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||
indexMap := make(map[string]int)
|
||||
|
||||
for _, item := range req.Items {
|
||||
if _, err := getWarehouse(item.WarehouseID); err != nil {
|
||||
_, pfkID, err := getWarehouse(item.WarehouseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -286,35 +308,42 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
productId: productId,
|
||||
warehouseId: warehouseId,
|
||||
subQty: item.Quantity,
|
||||
pfkID: pfkID,
|
||||
}
|
||||
aggregated = append(aggregated, entry)
|
||||
indexMap[key] = len(aggregated) - 1
|
||||
}
|
||||
|
||||
creditTermValue := req.CreditTerm
|
||||
creditTerm := &creditTermValue
|
||||
dueDateValue := time.Now().UTC().AddDate(0, 0, creditTermValue)
|
||||
dueDate := &dueDateValue
|
||||
var dueDate *time.Time
|
||||
if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" {
|
||||
parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate))
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid due_date, expected YYYY-MM-DD")
|
||||
}
|
||||
parsed = parsed.UTC()
|
||||
dueDate = &parsed
|
||||
}
|
||||
|
||||
purchase := &entity.Purchase{
|
||||
SupplierId: uint(req.SupplierID),
|
||||
CreditTerm: creditTerm,
|
||||
DueDate: dueDate,
|
||||
GrandTotal: 0,
|
||||
Notes: req.Notes,
|
||||
CreatedBy: uint(actorID),
|
||||
}
|
||||
|
||||
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
||||
emptyVehicle := ""
|
||||
for _, item := range aggregated {
|
||||
items = append(items, &entity.PurchaseItem{
|
||||
ProductId: item.productId,
|
||||
WarehouseId: item.warehouseId,
|
||||
ProjectFlockKandangId: item.pfkID,
|
||||
SubQty: item.subQty,
|
||||
TotalQty: 0,
|
||||
TotalUsed: 0,
|
||||
Price: 0,
|
||||
TotalPrice: 0,
|
||||
VehicleNumber: &emptyVehicle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -331,6 +360,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
return err
|
||||
}
|
||||
|
||||
if err := purchaseRepoTx.BackfillProjectFlockKandang(c.Context(), purchase.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID := uint(purchase.CreatedBy)
|
||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepPengajuan, entity.ApprovalActionCreated, actorID, nil, false); err != nil {
|
||||
return err
|
||||
@@ -361,6 +394,8 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
action, err := parseApprovalActionInput(req.Action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -371,7 +406,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
@@ -379,7 +414,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
|
||||
@@ -418,12 +453,10 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
grandTotalUpdated := false
|
||||
if len(payload.PricingUpdates) > 0 {
|
||||
if err := purchaseRepoTx.UpdatePricing(c.Context(), purchase.Id, payload.PricingUpdates, payload.GrandTotal); err != nil {
|
||||
if err := purchaseRepoTx.UpdatePricing(c.Context(), purchase.Id, payload.PricingUpdates); err != nil {
|
||||
return err
|
||||
}
|
||||
grandTotalUpdated = true
|
||||
}
|
||||
|
||||
if len(payload.NewItems) > 0 {
|
||||
@@ -432,12 +465,6 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
}
|
||||
}
|
||||
|
||||
if !grandTotalUpdated {
|
||||
if err := purchaseRepoTx.UpdateGrandTotal(c.Context(), purchase.Id, payload.GrandTotal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isInitialApproval {
|
||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepStaffPurchase, action, actorID, req.Notes, false); err != nil {
|
||||
return err
|
||||
@@ -481,17 +508,6 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||
}
|
||||
|
||||
if len(payload.NewItems) > 0 {
|
||||
newItems := make([]entity.PurchaseItem, len(payload.NewItems))
|
||||
for i, item := range payload.NewItems {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
newItems[i] = *item
|
||||
}
|
||||
s.notifyExpenseItemsCreated(c.Context(), purchase.Id, newItems)
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
@@ -611,6 +627,8 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
action, err := parseApprovalActionInput(req.Action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -621,7 +639,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
@@ -647,14 +665,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionRejected {
|
||||
if err := s.createPurchaseApproval(c.Context(), nil, purchase.Id, utils.PurchaseStepReceiving, action, actorID, req.Notes, true); err != nil {
|
||||
if err := s.createPurchaseApproval(ctx, nil, purchase.Id, utils.PurchaseStepReceiving, action, actorID, req.Notes, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
}
|
||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
return updated, nil
|
||||
@@ -670,6 +688,8 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
payload validation.ReceivePurchaseItemRequest
|
||||
receivedDate time.Time
|
||||
warehouseID uint
|
||||
supplierID uint
|
||||
transportPerItem *float64
|
||||
overrideWarehouse bool
|
||||
receivedQty float64
|
||||
}
|
||||
@@ -682,7 +702,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||
}
|
||||
|
||||
receivedDate, err := time.Parse("2006-01-02", payload.ReceivedDate)
|
||||
receivedDate, err := utils.ParseDateString(payload.ReceivedDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
||||
}
|
||||
@@ -697,6 +717,9 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
if warehouseID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
||||
}
|
||||
if payload.WarehouseID != nil && uint(item.WarehouseId) != warehouseID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving does not allow changing warehouse")
|
||||
}
|
||||
|
||||
var receivedQty float64
|
||||
if payload.ReceivedQty != nil {
|
||||
@@ -716,11 +739,27 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
}
|
||||
visitedItems[payload.PurchaseItemID] = struct{}{}
|
||||
|
||||
supplierID := purchase.SupplierId
|
||||
if payload.ExpeditionVendorID != nil && *payload.ExpeditionVendorID != 0 {
|
||||
supplierID = *payload.ExpeditionVendorID
|
||||
}
|
||||
|
||||
var transportPerItem *float64
|
||||
if payload.TransportPerItem != nil {
|
||||
if *payload.TransportPerItem < 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
||||
}
|
||||
val := *payload.TransportPerItem
|
||||
transportPerItem = &val
|
||||
}
|
||||
|
||||
prepared = append(prepared, preparedReceiving{
|
||||
item: item,
|
||||
payload: payload,
|
||||
receivedDate: receivedDate,
|
||||
warehouseID: warehouseID,
|
||||
supplierID: supplierID,
|
||||
transportPerItem: transportPerItem,
|
||||
overrideWarehouse: overrideWarehouse,
|
||||
receivedQty: receivedQty,
|
||||
})
|
||||
@@ -767,6 +806,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
deltas := make(map[uint]float64)
|
||||
affected := make(map[uint]struct{})
|
||||
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
||||
fifoAdds := make([]struct {
|
||||
itemID uint
|
||||
pwID uint
|
||||
qty float64
|
||||
}, 0, len(prepared))
|
||||
|
||||
for _, prep := range prepared {
|
||||
item := prep.item
|
||||
@@ -780,21 +824,29 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
var newPWID *uint
|
||||
clearPW := false
|
||||
|
||||
// Always ensure PW when qty > 0 so stockable has target.
|
||||
if prep.receivedQty > 0 {
|
||||
pwID, err := pwRepoTx.EnsureProductWarehouse(c.Context(), uint(item.ProductId), prep.warehouseID, purchase.CreatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newPWID = &pwID
|
||||
deltas[pwID] += prep.receivedQty
|
||||
affected[pwID] = struct{}{}
|
||||
} else {
|
||||
} else if oldPWID != nil {
|
||||
newPWID = oldPWID
|
||||
clearPW = true
|
||||
}
|
||||
|
||||
if oldPWID != nil {
|
||||
deltas[*oldPWID] -= item.TotalQty
|
||||
affected[*oldPWID] = struct{}{}
|
||||
deltaQty := prep.receivedQty - item.TotalQty
|
||||
switch {
|
||||
case deltaQty > 0 && newPWID != nil:
|
||||
fifoAdds = append(fifoAdds, struct {
|
||||
itemID uint
|
||||
pwID uint
|
||||
qty float64
|
||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||
case deltaQty < 0 && newPWID != nil:
|
||||
deltas[*newPWID] += deltaQty // negative
|
||||
affected[*newPWID] = struct{}{}
|
||||
}
|
||||
|
||||
dateCopy := prep.receivedDate
|
||||
@@ -830,12 +882,21 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepReceiving, receivingAction, actorID, req.Notes, true); err != nil {
|
||||
if s.FifoSvc != nil {
|
||||
for _, adj := range fifoAdds {
|
||||
if adj.pwID == 0 || adj.qty <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: purchaseStockableKey,
|
||||
StockableID: adj.itemID,
|
||||
ProductWarehouseID: adj.pwID,
|
||||
Quantity: adj.qty,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.createPurchaseApproval(c.Context(), tx, purchase.Id, utils.PurchaseStepCompleted, completedAction, actorID, req.Notes, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -863,12 +924,28 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
PurchaseItemID: prep.item.Id,
|
||||
ProductID: prep.item.ProductId,
|
||||
WarehouseID: uint(prep.warehouseID),
|
||||
SupplierID: prep.supplierID,
|
||||
TransportPerItem: prep.transportPerItem,
|
||||
ReceivedQty: prep.receivedQty,
|
||||
ReceivedDate: &date,
|
||||
}
|
||||
receivingPayloads = append(receivingPayloads, payload)
|
||||
}
|
||||
s.notifyExpenseItemsReceived(c.Context(), purchase.Id, receivingPayloads)
|
||||
if err := s.notifyExpenseItemsReceived(c, purchase.Id, receivingPayloads); err != nil {
|
||||
s.Log.Errorf("Failed to sync expense for purchase %d: %+v", purchase.Id, err)
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return nil, fe
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
}
|
||||
|
||||
// Create approvals only after expense sync succeeds
|
||||
if err := s.createPurchaseApproval(c.Context(), nil, purchase.Id, utils.PurchaseStepReceiving, receivingAction, actorID, req.Notes, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.createPurchaseApproval(c.Context(), nil, purchase.Id, utils.PurchaseStepCompleted, completedAction, actorID, req.Notes, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
@@ -918,6 +995,17 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Requested items were not found in this purchase")
|
||||
}
|
||||
|
||||
toDeleteSet := make(map[uint]struct{}, len(toDelete))
|
||||
for _, id := range toDelete {
|
||||
toDeleteSet[id] = struct{}{}
|
||||
}
|
||||
itemsToDelete := make([]entity.PurchaseItem, 0, len(toDelete))
|
||||
for _, item := range purchase.Items {
|
||||
if _, ok := toDeleteSet[item.Id]; ok {
|
||||
itemsToDelete = append(itemsToDelete, item)
|
||||
}
|
||||
}
|
||||
|
||||
if len(purchase.Items)-len(toDelete) <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must keep at least one item")
|
||||
}
|
||||
@@ -929,10 +1017,6 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repoTx.UpdateGrandTotal(ctx, purchase.Id, remainingTotal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
@@ -942,8 +1026,14 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase items")
|
||||
}
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
s.notifyExpenseItemsDeleted(ctx, purchase.Id, toDelete)
|
||||
if len(itemsToDelete) > 0 {
|
||||
if err := s.notifyExpenseItemsDeleted(ctx, purchase.Id, itemsToDelete); err != nil {
|
||||
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", purchase.Id, err)
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return nil, fe
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||
@@ -971,9 +1061,9 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
}
|
||||
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
for _, item := range purchase.Items {
|
||||
itemIDs = append(itemIDs, item.Id)
|
||||
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
|
||||
for i, item := range purchase.Items {
|
||||
itemsToDelete[i] = item
|
||||
}
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -995,38 +1085,87 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase")
|
||||
}
|
||||
|
||||
if len(itemIDs) > 0 {
|
||||
s.notifyExpenseItemsDeleted(ctx, uint(id), itemIDs)
|
||||
if len(itemsToDelete) > 0 {
|
||||
if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil {
|
||||
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err)
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return fe
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) notifyExpenseItemsCreated(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) {
|
||||
if s.ExpenseBridge == nil || purchaseID == 0 || len(items) == 0 {
|
||||
return
|
||||
func (s *purchaseService) createPurchaseApproval(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
purchaseID uint,
|
||||
step approvalutils.ApprovalStep,
|
||||
action entity.ApprovalAction,
|
||||
actorID uint,
|
||||
notes *string,
|
||||
allowDuplicate bool,
|
||||
) error {
|
||||
if purchaseID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval")
|
||||
}
|
||||
if err := s.ExpenseBridge.OnItemsCreated(ctx, purchaseID, items); err != nil {
|
||||
s.Log.Warnf("Failed to notify expense bridge for created purchase %d: %+v", purchaseID, err)
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
|
||||
svc := s.approvalServiceForDB(db)
|
||||
if svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Approval service not available")
|
||||
}
|
||||
|
||||
modifier := func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("step_number = ?", uint16(step))
|
||||
}
|
||||
|
||||
latest, err := svc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), modifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !allowDuplicate && latest != nil &&
|
||||
latest.Action != nil &&
|
||||
*latest.Action == action {
|
||||
return nil
|
||||
}
|
||||
|
||||
actionCopy := action
|
||||
_, err = svc.CreateApproval(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), step, &actionCopy, actorID, notes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *purchaseService) notifyExpenseItemsReceived(ctx context.Context, purchaseID uint, payloads []ExpenseReceivingPayload) {
|
||||
func (s *purchaseService) approvalServiceForDB(db *gorm.DB) commonSvc.ApprovalService {
|
||||
if db != nil {
|
||||
return commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
||||
}
|
||||
if s.ApprovalSvc != nil {
|
||||
return s.ApprovalSvc
|
||||
}
|
||||
if s.PurchaseRepo != nil && s.PurchaseRepo.DB() != nil {
|
||||
return commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.PurchaseRepo.DB()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) notifyExpenseItemsReceived(c *fiber.Ctx, purchaseID uint, payloads []ExpenseReceivingPayload) error {
|
||||
if s.ExpenseBridge == nil || purchaseID == 0 || len(payloads) == 0 {
|
||||
return
|
||||
}
|
||||
if err := s.ExpenseBridge.OnItemsReceived(ctx, purchaseID, payloads); err != nil {
|
||||
s.Log.Warnf("Failed to notify expense bridge for received purchase %d: %+v", purchaseID, err)
|
||||
return nil
|
||||
}
|
||||
return s.ExpenseBridge.OnItemsReceived(c, purchaseID, payloads)
|
||||
}
|
||||
|
||||
func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchaseID uint, itemIDs []uint) {
|
||||
if s.ExpenseBridge == nil || purchaseID == 0 || len(itemIDs) == 0 {
|
||||
return
|
||||
}
|
||||
if err := s.ExpenseBridge.OnItemsDeleted(ctx, purchaseID, itemIDs); err != nil {
|
||||
s.Log.Warnf("Failed to notify expense bridge for deleted purchase %d: %+v", purchaseID, err)
|
||||
func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error {
|
||||
if s.ExpenseBridge == nil || purchaseID == 0 || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ExpenseBridge.OnItemsDeleted(ctx, purchaseID, items)
|
||||
|
||||
}
|
||||
|
||||
func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
@@ -1054,7 +1193,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
}
|
||||
|
||||
updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items))
|
||||
var grandTotal float64
|
||||
|
||||
existingCombos := make(map[string]struct{}, len(purchase.Items)+len(newPayloads))
|
||||
for _, item := range purchase.Items {
|
||||
@@ -1120,7 +1258,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
}
|
||||
|
||||
updates = append(updates, update)
|
||||
grandTotal += totalPrice
|
||||
delete(requestItems, item.Id)
|
||||
}
|
||||
if len(requestItems) > 0 {
|
||||
@@ -1129,6 +1266,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
|
||||
productSupplierCache := make(map[uint]bool)
|
||||
newItems := make([]*entity.PurchaseItem, 0, len(newPayloads))
|
||||
emptyVehicle := ""
|
||||
|
||||
for _, payload := range newPayloads {
|
||||
if payload.ProductID == 0 || payload.WarehouseID == 0 {
|
||||
@@ -1183,10 +1321,10 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
TotalUsed: 0,
|
||||
Price: payload.Price,
|
||||
TotalPrice: totalPrice,
|
||||
VehicleNumber: &emptyVehicle,
|
||||
}
|
||||
newItems = append(newItems, newItem)
|
||||
existingCombos[key] = struct{}{}
|
||||
grandTotal += totalPrice
|
||||
}
|
||||
|
||||
if len(updates) == 0 && len(newItems) == 0 {
|
||||
@@ -1196,7 +1334,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
return &staffAdjustmentPayload{
|
||||
PricingUpdates: updates,
|
||||
NewItems: newItems,
|
||||
GrandTotal: grandTotal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1239,36 +1376,6 @@ func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseQueryDates(fromStr, toStr string) (*time.Time, *time.Time, error) {
|
||||
var fromPtr *time.Time
|
||||
var toPtr *time.Time
|
||||
const queryDateLayout = "2006-01-02"
|
||||
|
||||
if strings.TrimSpace(fromStr) != "" {
|
||||
parsed, err := time.Parse(queryDateLayout, fromStr)
|
||||
if err != nil {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_from must use format YYYY-MM-DD")
|
||||
}
|
||||
fromValue := parsed
|
||||
fromPtr = &fromValue
|
||||
}
|
||||
|
||||
if strings.TrimSpace(toStr) != "" {
|
||||
parsed, err := time.Parse(queryDateLayout, toStr)
|
||||
if err != nil {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_to must use format YYYY-MM-DD")
|
||||
}
|
||||
toValue := parsed.AddDate(0, 0, 1)
|
||||
toPtr = &toValue
|
||||
}
|
||||
|
||||
if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "created_from must be earlier than created_to")
|
||||
}
|
||||
|
||||
return fromPtr, toPtr, nil
|
||||
}
|
||||
|
||||
func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
||||
value := strings.ToUpper(strings.TrimSpace(raw))
|
||||
switch value {
|
||||
@@ -1302,53 +1409,3 @@ func (s *purchaseService) rejectAndReload(
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) createPurchaseApproval(
|
||||
ctx context.Context,
|
||||
db *gorm.DB,
|
||||
purchaseID uint,
|
||||
step approvalutils.ApprovalStep,
|
||||
action entity.ApprovalAction,
|
||||
actorID uint,
|
||||
notes *string,
|
||||
allowDuplicate bool,
|
||||
) error {
|
||||
if purchaseID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval")
|
||||
}
|
||||
if actorID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "ActorId is invalid for approval")
|
||||
}
|
||||
|
||||
var svc commonSvc.ApprovalService
|
||||
switch {
|
||||
case db != nil:
|
||||
svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
||||
case s.ApprovalSvc != nil:
|
||||
svc = s.ApprovalSvc
|
||||
case s.PurchaseRepo != nil && s.PurchaseRepo.DB() != nil:
|
||||
svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.PurchaseRepo.DB()))
|
||||
}
|
||||
if svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Approval service not available")
|
||||
}
|
||||
|
||||
modifier := func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("step_number = ?", uint16(step))
|
||||
}
|
||||
|
||||
latest, err := svc.LatestByTarget(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), modifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !allowDuplicate && latest != nil &&
|
||||
latest.Action != nil &&
|
||||
*latest.Action == action {
|
||||
return nil
|
||||
}
|
||||
|
||||
actionCopy := action
|
||||
_, err = svc.CreateApproval(ctx, utils.ApprovalWorkflowPurchase, uint(purchaseID), step, &actionCopy, actorID, notes)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ type PurchaseItemPayload struct {
|
||||
|
||||
type CreatePurchaseRequest struct {
|
||||
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||
CreditTerm int `json:"credit_term" validate:"required,gte=0"`
|
||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||
}
|
||||
@@ -38,6 +38,8 @@ type ReceivePurchaseItemRequest struct {
|
||||
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"`
|
||||
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
|
||||
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
|
||||
ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
|
||||
TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
|
||||
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
|
||||
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
|
||||
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
|
||||
|
||||
@@ -30,7 +30,7 @@ func (r *StockLogRepositoryImpl) GetByFlaggable(ctx context.Context, logType str
|
||||
var stockLogs []*entity.StockLog
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("log_type = ? AND log_id = ?", logType, logId).
|
||||
Where("loggable_type = ? AND log_id = ?", logType, logId).
|
||||
Preload("ProductWarehouse").
|
||||
Preload("ProductWarehouse.Product").
|
||||
Preload("ProductWarehouse.Warehouse").
|
||||
|
||||
@@ -216,13 +216,11 @@ var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
|
||||
const (
|
||||
ApprovalWorkflowRecording approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("RECORDINGS")
|
||||
RecordingStepGradingTelur approvalutils.ApprovalStep = 1
|
||||
RecordingStepPengajuan approvalutils.ApprovalStep = 2
|
||||
RecordingStepDisetujui approvalutils.ApprovalStep = 3
|
||||
RecordingStepPengajuan approvalutils.ApprovalStep = 1
|
||||
RecordingStepDisetujui approvalutils.ApprovalStep = 2
|
||||
)
|
||||
|
||||
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
RecordingStepGradingTelur: "Grading-Telur",
|
||||
RecordingStepPengajuan: "Pengajuan",
|
||||
RecordingStepDisetujui: "Disetujui",
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
Qty: item.Qty,
|
||||
Weight: item.Weight,
|
||||
CreatedBy: createdBy,
|
||||
})
|
||||
}
|
||||
|
||||
+34
-1
@@ -1,8 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseDateString mengubah string "YYYY-MM-DD" menjadi time.Time
|
||||
@@ -23,3 +24,35 @@ func ParseDateString(dateStr string) (time.Time, error) {
|
||||
func FormatDate(t time.Time) string {
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
// ParseDateRangeForQuery parses optional YYYY-MM-DD from/to strings for list filters.
|
||||
// It returns a start pointer (inclusive) and an end pointer advanced by one day
|
||||
// so callers can safely use "< end" to achieve an inclusive upper bound.
|
||||
func ParseDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, error) {
|
||||
var fromPtr *time.Time
|
||||
var toPtr *time.Time
|
||||
|
||||
if strings.TrimSpace(fromStr) != "" {
|
||||
parsed, err := ParseDateString(strings.TrimSpace(fromStr))
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("created_from must use format YYYY-MM-DD")
|
||||
}
|
||||
fromValue := parsed
|
||||
fromPtr = &fromValue
|
||||
}
|
||||
|
||||
if strings.TrimSpace(toStr) != "" {
|
||||
parsed, err := ParseDateString(strings.TrimSpace(toStr))
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("created_to must use format YYYY-MM-DD")
|
||||
}
|
||||
nextDay := parsed.AddDate(0, 0, 1)
|
||||
toPtr = &nextDay
|
||||
}
|
||||
|
||||
if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) {
|
||||
return nil, nil, errors.New("created_from must be earlier than created_to")
|
||||
}
|
||||
|
||||
return fromPtr, toPtr, nil
|
||||
}
|
||||
|
||||
@@ -263,7 +263,6 @@ func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.Pr
|
||||
ProductId: 1,
|
||||
WarehouseId: 1,
|
||||
Quantity: qty,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
if err := db.Create(&pw).Error; err != nil {
|
||||
t.Fatalf("create product warehouse: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user