Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2025-12-31 10:29:32 +07:00
40 changed files with 2973 additions and 369 deletions
+7
View File
@@ -19,6 +19,7 @@ require (
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.33.0
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
@@ -71,9 +72,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -82,12 +86,15 @@ require (
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect github.com/tinylib/msgp v1.1.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+21 -1
View File
@@ -182,6 +182,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
@@ -195,6 +197,11 @@ github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -238,8 +245,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
@@ -252,6 +260,16 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -278,6 +296,8 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -0,0 +1,6 @@
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_deleted_at;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_created_by;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id;
DROP TABLE IF EXISTS project_flock_kandang_uniformity;
@@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS project_flock_kandang_uniformity (
id BIGSERIAL PRIMARY KEY,
uniformity NUMERIC(15, 3),
week INT NOT NULL,
cv NUMERIC(15, 3),
chick_qty_of_weight NUMERIC(15, 3),
mean_up NUMERIC(15, 3),
mean_down NUMERIC(15, 3),
project_flock_kandang_id BIGINT NOT NULL,
uniform_qty NUMERIC(15, 3),
not_uniform_qty NUMERIC(15, 3),
uniform_date TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
EXECUTE
'ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_created_by'
) THEN
EXECUTE
'ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id
ON project_flock_kandang_uniformity (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week
ON project_flock_kandang_uniformity (project_flock_kandang_id, week);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_created_by
ON project_flock_kandang_uniformity (created_by);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_deleted_at
ON project_flock_kandang_uniformity (deleted_at);
@@ -0,0 +1,54 @@
BEGIN;
CREATE TABLE IF NOT EXISTS recording_bws (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
avg_weight NUMERIC(8,2) NOT NULL,
qty NUMERIC(15,3) NOT NULL DEFAULT 1,
total_weight NUMERIC(10,3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_bws_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT chk_recording_bws_nonneg
CHECK (avg_weight >= 0 AND qty >= 0 AND total_weight >= 0)
);
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
ON recording_bws (recording_id);
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS hand_day,
DROP COLUMN IF EXISTS hand_house,
DROP COLUMN IF EXISTS feed_intake,
DROP COLUMN IF EXISTS egg_mesh,
DROP COLUMN IF EXISTS egg_weight;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0)
);
ALTER TABLE recording_eggs
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
ALTER TABLE recording_eggs
ALTER COLUMN weight TYPE NUMERIC(10,3) USING weight::NUMERIC(10,3);
ALTER TABLE recording_eggs
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
qty >= 0 AND (weight IS NULL OR weight >= 0)
);
COMMIT;
@@ -0,0 +1,44 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS hand_day NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS hand_house NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS feed_intake NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS egg_mesh NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS egg_weight NUMERIC(15,3);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hand_day IS NULL OR hand_day >= 0) AND
(hand_house IS NULL OR hand_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mesh IS NULL OR egg_mesh >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
ALTER TABLE recording_eggs
ALTER COLUMN weight TYPE NUMERIC(15,3) USING weight::NUMERIC(15,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)
);
DROP INDEX IF EXISTS idx_recording_bws_recording;
DROP TABLE IF EXISTS recording_bws;
COMMIT;
@@ -0,0 +1,33 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlockKandangUniformity struct {
Id uint `gorm:"primaryKey"`
Uniformity float64 `gorm:"type:numeric(15,3)"`
Week int `gorm:"not null"`
Cv float64 `gorm:"type:numeric(15,3)"`
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
MeanUp float64 `gorm:"type:numeric(15,3)"`
MeanDown float64 `gorm:"type:numeric(15,3)"`
ProjectFlockKandangId uint `gorm:"not null"`
UniformQty float64 `gorm:"type:numeric(15,3)"`
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
UniformDate *time.Time `gorm:"type:timestamptz"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"not null"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
func (ProjectFlockKandangUniformity) TableName() string {
return "project_flock_kandang_uniformity"
}
+13 -3
View File
@@ -13,11 +13,14 @@ type Recording struct {
Day *int `gorm:"column:day"` Day *int `gorm:"column:day"`
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
DailyGain *float64 `gorm:"column:daily_gain"`
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
CumIntake *int `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
HandDay *float64 `gorm:"column:hand_day"`
HandHouse *float64 `gorm:"column:hand_house"`
FeedIntake *float64 `gorm:"column:feed_intake"`
EggMesh *float64 `gorm:"column:egg_mesh"`
EggWeight *float64 `gorm:"column:egg_weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -25,10 +28,17 @@ type Recording struct {
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"` Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"` Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"` Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
StandardHandDay *float64 `gorm:"-"`
StandardHandHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMesh *float64 `gorm:"-"`
StandardEggWeight *float64 `gorm:"-"`
StandardFcr *float64 `gorm:"-"`
} }
-15
View File
@@ -1,15 +0,0 @@
package entities
import "time"
type RecordingBW struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
AvgWeight float64 `gorm:"column:avg_weight;not null"`
Qty float64 `gorm:"column:qty;not null"`
TotalWeight float64 `gorm:"column:total_weight;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Uniformity struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+8 -6
View File
@@ -44,6 +44,7 @@ const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
) )
const ( const (
@@ -218,12 +219,13 @@ const (
) )
const ( const (
P_FinanceGetAll = "lti.finance.list" P_Uniformities_GetAll = "lti.production.uniformity.list"
P_FinanceGetOne = "lti.finance.detail" P_Uniformities_GetOne = "lti.production.uniformity.detail"
P_FinanceCreateOne = "lti.finance.create" P_Uniformities_Verify = "lti.production.uniformity.verify"
P_FinanceUpdateOne = "lti.finance.update" P_Uniformities_CreateOne = "lti.production.uniformity.create"
P_FinanceDeleteOne = "lti.finance.delete" P_Uniformities_UpdateOne = "lti.production.uniformity.update"
P_FinanceApproval = "lti.finance.approve" P_Uniformities_DeleteOne = "lti.production.uniformity.delete"
P_Uniformities_Approval = "lti.production.uniformity.approve"
) )
const ( const (
@@ -215,21 +215,19 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
} }
if len(activeProjectFlocks) == 0 { if len(activeProjectFlocks) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "No active project flocks found for this location") projectFlockIDs := make([]uint64, len(activeProjectFlocks))
} for i, pf := range activeProjectFlocks {
projectFlockIDs[i] = uint64(pf.Id)
}
projectFlockIDs := make([]uint64, len(activeProjectFlocks)) projectFlockIdsJSON, err := json.Marshal(projectFlockIDs)
for i, pf := range activeProjectFlocks { if err != nil {
projectFlockIDs[i] = uint64(pf.Id) return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids")
}
jsonStr := string(projectFlockIdsJSON)
projectFlockIdJSON = &jsonStr
} }
projectFlockIdsJSON, err := json.Marshal(projectFlockIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids")
}
jsonStr := string(projectFlockIdsJSON)
projectFlockIdJSON = &jsonStr
} }
expense = &entity.Expense{ expense = &entity.Expense{
@@ -29,6 +29,8 @@ type ProductWarehouseRepository interface {
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error) EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
GetByProductWarehouseAndProjectFlockKandang(ctx context.Context, productId, warehouseId, projectFlockKandangId uint) (*entity.ProductWarehouse, error)
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -214,6 +216,31 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
return nil return nil
} }
var inUseIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs).
Distinct().
Pluck("product_warehouse_id", &inUseIDs).Error; err != nil {
return err
}
if len(inUseIDs) > 0 {
inUse := make(map[uint]struct{}, len(inUseIDs))
for _, id := range inUseIDs {
inUse[id] = struct{}{}
}
filtered := make([]uint, 0, len(emptyIDs))
for _, id := range emptyIDs {
if _, exists := inUse[id]; !exists {
filtered = append(filtered, id)
}
}
emptyIDs = filtered
}
if len(emptyIDs) == 0 {
return nil
}
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}). Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs). Where("product_warehouse_id IN ?", emptyIDs).
@@ -272,6 +299,30 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
return entity.Id, nil return entity.Id, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang(
ctx context.Context,
productId uint,
warehouseId uint,
projectFlockKandangId uint,
) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
if err := r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id = ?", productId, warehouseId, projectFlockKandangId).
First(&productWarehouse).Error; err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
if len(projectFlockKandangIDs) == 0 {
return nil
}
return r.DB().WithContext(ctx).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Delete(&entity.ProductWarehouse{}).Error
}
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -268,6 +268,7 @@ func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error {
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
projectFlockId := c.QueryInt("project_flock_id", 0) projectFlockId := c.QueryInt("project_flock_id", 0)
kandangId := c.QueryInt("kandang_id", 0) kandangId := c.QueryInt("kandang_id", 0)
withPopulation := c.QueryBool("withpopulation", false)
if projectFlockId == 0 || kandangId == 0 { if projectFlockId == 0 || kandangId == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id") return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
@@ -280,6 +281,13 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
dtoResult := dto.ToProjectFlockKandangDTO(*result) dtoResult := dto.ToProjectFlockKandangDTO(*result)
dtoResult.AvailableQuantity = float64(availableStock) dtoResult.AvailableQuantity = float64(availableStock)
if withPopulation {
population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id)
if err != nil {
return err
}
dtoResult.Population = &population
}
if dtoResult.ProjectFlock != nil { if dtoResult.ProjectFlock != nil {
for i := range dtoResult.ProjectFlock.Kandangs { for i := range dtoResult.ProjectFlock.Kandangs {
@@ -34,6 +34,7 @@ type ProjectFlockKandangDTO struct {
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
AvailableQuantity float64 `json:"available_quantity"` AvailableQuantity float64 `json:"available_quantity"`
Population *float64 `json:"population,omitempty"`
} }
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
@@ -16,6 +16,7 @@ import (
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/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" 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" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -32,6 +33,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
nonstockRepo := rNonstock.NewNonstockRepository(db) nonstockRepo := rNonstock.NewNonstockRepository(db)
projectflockRepo := rProjectflock.NewProjectflockRepository(db) projectflockRepo := rProjectflock.NewProjectflockRepository(db)
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db) projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
@@ -43,7 +46,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
} }
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate) projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService) ProjectflockRoutes(router, userService, projectflockService)
@@ -15,6 +15,7 @@ type ProjectFlockPopulationRepository interface {
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
// subset of base repository methods used by services // subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
@@ -106,3 +107,20 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
} }
return total, nil return total, nil
} }
func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
var total float64
err := r.DB().WithContext(ctx).
Table("project_flock_populations").
Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS total_qty").
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
Scan(&total).Error
if err != nil {
return 0, err
}
if total < 0 {
total = 0
}
return total, nil
}
@@ -27,6 +27,7 @@ type ProjectFlockKandangRepository interface {
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
@@ -89,6 +90,20 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont
return records, nil return records, nil
} }
func (r *projectFlockKandangRepositoryImpl) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
if len(kandangIDs) == 0 {
return []uint{}, nil
}
var ids []uint
if err := r.db.WithContext(ctx).
Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
Pluck("id", &ids).Error; err != nil {
return nil, err
}
return ids, nil
}
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) { func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
var records []entity.ProjectFlockKandang var records []entity.ProjectFlockKandang
var total int64 var total int64
@@ -21,6 +21,7 @@ import (
repository "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" 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" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -37,6 +38,7 @@ type ProjectflockService interface {
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
@@ -54,6 +56,8 @@ type projectflockService struct {
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
PivotRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
PopulationRepo repository.ProjectFlockPopulationRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
@@ -73,6 +77,8 @@ func NewProjectflockService(
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository, productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository, projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
nonstockRepo nonstockRepository.NonstockRepository, nonstockRepo nonstockRepository.NonstockRepository,
populationRepo repository.ProjectFlockPopulationRepository,
recordingRepo recordingRepo.RecordingRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
validate *validator.Validate, validate *validator.Validate,
@@ -86,7 +92,10 @@ func NewProjectflockService(
NonstockRepo: nonstockRepo, NonstockRepo: nonstockRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProjectBudgetRepo: projectBudgetRepo,
PivotRepo: pivotRepo, PivotRepo: pivotRepo,
PopulationRepo: populationRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock, approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
@@ -419,6 +428,34 @@ func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fibe
return pfk, availableQuantity, nil return pfk, availableQuantity, nil
} }
func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) {
if s.PopulationRepo == nil {
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
}
if projectFlockKandangID == 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
if s.RecordingRepo != nil {
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
}
if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 {
return *latest.TotalChickQty, nil
}
}
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
}
return total, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) { func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
idStr = strings.TrimSpace(idStr) idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr) projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
@@ -795,6 +832,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
} }
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
} }
if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil {
return err
}
return nil return nil
} }
@@ -820,6 +860,23 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", "))) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
} }
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids")
}
if len(pfkIDs) > 0 {
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
} else if pwRepo == nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
}
if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang")
}
}
if resetStatus { if resetStatus {
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil { if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
@@ -856,6 +913,81 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
return kandangRepository.NewKandangRepository(s.Repository.DB()) return kandangRepository.NewKandangRepository(s.Repository.DB())
} }
func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error {
if len(records) == 0 {
return nil
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
} else if pwRepo == nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
}
warehouseRepo := s.WarehouseRepo
if dbTransaction != nil {
warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction)
} else if warehouseRepo == nil {
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
}
flags := []utils.FlagType{
utils.FlagAyamAfkir,
utils.FlagAyamCulling,
utils.FlagAyamMati,
utils.FlagTelurPecah,
utils.FlagTelurUtuh,
}
productIDs := make(map[utils.FlagType]uint, len(flags))
for _, flag := range flags {
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag))
}
return err
}
productIDs[flag] = product.Id
}
for _, record := range records {
if record == nil || record.Id == 0 {
continue
}
warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId))
}
return err
}
for _, flag := range flags {
productID := productIDs[flag]
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
newPW := entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouse.Id,
ProjectFlockKandangId: &record.Id,
Quantity: 0,
}
if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil {
return err
}
}
}
return nil
}
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) { func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
@@ -22,11 +22,21 @@ type RecordingRelationDTO struct {
ProjectFlockCategory string `json:"project_flock_category"` ProjectFlockCategory string `json:"project_flock_category"`
TotalDepletionQty float64 `json:"total_depletion_qty"` TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"` CumDepletionRate float64 `json:"cum_depletion_rate"`
DailyGain float64 `json:"daily_gain"`
AvgDailyGain float64 `json:"avg_daily_gain"`
CumIntake int `json:"cum_intake"` CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"` FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"` TotalChickQty float64 `json:"total_chick_qty"`
HandDay float64 `json:"hand_day"`
HandHouse float64 `json:"hand_house"`
FeedIntake float64 `json:"feed_intake"`
EggMesh float64 `json:"egg_mesh"`
EggWeight float64 `json:"egg_weight"`
StandardHandDay *float64 `json:"hand_day_std,omitempty"`
StandardHandHouse *float64 `json:"hand_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"` Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
} }
@@ -39,16 +49,9 @@ type RecordingListDTO struct {
type RecordingDetailDTO struct { type RecordingDetailDTO struct {
RecordingListDTO RecordingListDTO
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"` Depletions []RecordingDepletionDTO `json:"depletions"`
Depletions []RecordingDepletionDTO `json:"depletions"` Stocks []RecordingStockDTO `json:"stocks"`
Stocks []RecordingStockDTO `json:"stocks"` Eggs []RecordingEggDTO `json:"eggs"`
Eggs []RecordingEggDTO `json:"eggs"`
}
type RecordingBodyWeightDTO struct {
AvgWeight float64 `json:"avg_weight"`
Qty float64 `json:"qty"`
TotalWeight float64 `json:"total_weight"`
} }
type RecordingDepletionDTO struct { type RecordingDepletionDTO struct {
@@ -88,11 +91,14 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
day int day int
totalDepletionQty float64 totalDepletionQty float64
cumDepletionRate float64 cumDepletionRate float64
dailyGain float64
avgDailyGain float64
cumIntake int cumIntake int
fcrValue float64 fcrValue float64
totalChickQty float64 totalChickQty float64
handDay float64
handHouse float64
feedIntake float64
eggMesh float64
eggWeight float64
) )
if e.Day != nil { if e.Day != nil {
@@ -104,12 +110,6 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
if e.CumDepletionRate != nil { if e.CumDepletionRate != nil {
cumDepletionRate = *e.CumDepletionRate cumDepletionRate = *e.CumDepletionRate
} }
if e.DailyGain != nil {
dailyGain = *e.DailyGain
}
if e.AvgDailyGain != nil {
avgDailyGain = *e.AvgDailyGain
}
if e.CumIntake != nil { if e.CumIntake != nil {
cumIntake = *e.CumIntake cumIntake = *e.CumIntake
} }
@@ -119,6 +119,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
if e.TotalChickQty != nil { if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty totalChickQty = *e.TotalChickQty
} }
if e.HandDay != nil {
handDay = *e.HandDay
}
if e.HandHouse != nil {
handHouse = *e.HandHouse
}
if e.FeedIntake != nil {
feedIntake = *e.FeedIntake
}
if e.EggMesh != nil {
eggMesh = *e.EggMesh
}
if e.EggWeight != nil {
eggWeight = *e.EggWeight
}
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 { if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category category := e.ProjectFlockKandang.ProjectFlock.Category
@@ -139,11 +154,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
ProjectFlockCategory: projectFlockCategory, ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: totalDepletionQty, TotalDepletionQty: totalDepletionQty,
CumDepletionRate: cumDepletionRate, CumDepletionRate: cumDepletionRate,
DailyGain: dailyGain,
AvgDailyGain: avgDailyGain,
CumIntake: cumIntake, CumIntake: cumIntake,
FcrValue: fcrValue, FcrValue: fcrValue,
TotalChickQty: totalChickQty, TotalChickQty: totalChickQty,
HandDay: handDay,
HandHouse: handHouse,
FeedIntake: feedIntake,
EggMesh: eggMesh,
EggWeight: eggWeight,
StandardHandDay: e.StandardHandDay,
StandardHandHouse: e.StandardHandHouse,
StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMesh: e.StandardEggMesh,
StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr,
Approval: latestApproval, Approval: latestApproval,
} }
} }
@@ -183,25 +208,12 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
return RecordingDetailDTO{ return RecordingDetailDTO{
RecordingListDTO: listDTO, RecordingListDTO: listDTO,
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
Depletions: ToRecordingDepletionDTOs(e.Depletions), Depletions: ToRecordingDepletionDTOs(e.Depletions),
Stocks: ToRecordingStockDTOs(e.Stocks), Stocks: ToRecordingStockDTOs(e.Stocks),
Eggs: eggs, Eggs: eggs,
} }
} }
func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO {
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
for i, bw := range bodyWeights {
result[i] = RecordingBodyWeightDTO{
AvgWeight: bw.AvgWeight,
Qty: bw.Qty,
TotalWeight: bw.TotalWeight,
}
}
return result
}
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO { func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
result := make([]RecordingDepletionDTO, len(depletions)) result := make([]RecordingDepletionDTO, len(depletions))
for i, d := range depletions { for i, d := range depletions {
@@ -17,11 +17,9 @@ type RecordingRepository interface {
repository.BaseRepository[entity.Recording] repository.BaseRepository[entity.Recording]
WithRelations(db *gorm.DB) *gorm.DB WithRelations(db *gorm.DB) *gorm.DB
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
@@ -41,10 +39,11 @@ type RecordingRepository interface {
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error)
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
@@ -66,7 +65,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("CreatedUser"). Preload("CreatedUser").
Preload("ProjectFlockKandang"). Preload("ProjectFlockKandang").
Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock").
Preload("BodyWeights").
Preload("Depletions"). Preload("Depletions").
Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse").
Preload("Depletions.ProductWarehouse.Product"). Preload("Depletions.ProductWarehouse.Product").
@@ -81,6 +79,27 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
Preload("Eggs.ProductWarehouse.Warehouse") Preload("Eggs.ProductWarehouse.Warehouse")
} }
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
if projectFlockKandangId == 0 {
return nil, errors.New("project_flock_kandang_id is required")
}
var record entity.Recording
err := r.DB().WithContext(ctx).
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Order("record_datetime DESC").
Order("created_at DESC").
Limit(1).
Find(&record).Error
if errors.Is(err, gorm.ErrRecordNotFound) || record.Id == 0 {
return nil, nil
}
if err != nil {
return nil, err
}
return &record, nil
}
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) { func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
var days []int var days []int
if err := tx.Model(&entity.Recording{}). if err := tx.Model(&entity.Recording{}).
@@ -92,17 +111,6 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
return nextRecordingDay(days), nil return nextRecordingDay(days), nil
} }
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
if len(bodyWeights) == 0 {
return nil
}
return tx.Create(&bodyWeights).Error
}
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
}
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error { func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 { if len(stocks) == 0 {
return nil return nil
@@ -271,21 +279,18 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang
return int64(math.Round(total)), nil return int64(math.Round(total)), nil
} }
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) {
var result struct { if projectFlockKandangId == 0 {
TotalWeight float64
TotalQty float64
}
if err := tx.Model(&entity.RecordingBW{}).
Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
Where("recording_id = ?", recordingID).
Scan(&result).Error; err != nil {
return 0, err
}
if result.TotalQty == 0 {
return 0, nil return 0, nil
} }
return result.TotalWeight / result.TotalQty, nil
var result float64
err := tx.
Table("project_chickins").
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
Scan(&result).Error
return result, err
} }
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
@@ -322,22 +327,48 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
return total, nil return total, nil
} }
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) { func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
if recordingID == 0 {
return 0, 0, nil
}
var result struct { var result struct {
FcrID uint TotalQty float64
TotalWeightGrams float64
} }
if err := tx.Table("project_flock_kandangs"). err = tx.
Select("project_flocks.fcr_id AS fcr_id"). Table("recording_eggs").
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(recording_eggs.qty * COALESCE(recording_eggs.weight, 0)), 0) AS total_weight_grams").
Where("project_flock_kandangs.id = ?", projectFlockKandangId). Where("recording_eggs.recording_id = ?", recordingID).
Scan(&result).Error; err != nil { Scan(&result).Error
return 0, err if err != nil {
return 0, 0, err
} }
return result.FcrID, nil return result.TotalQty, result.TotalWeightGrams, nil
} }
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) { func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang(
if fcrId == 0 { tx *gorm.DB,
projectFlockKandangId uint,
recordTime time.Time,
) (float64, error) {
if projectFlockKandangId == 0 {
return 0, nil
}
var result float64
err := tx.
Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId).
Where("recordings.record_datetime <= ?", recordTime).
Scan(&result).Error
return result, err
}
func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
if fcrId == 0 || currentWeightKg <= 0 {
return 0, false, nil return 0, false, nil
} }
@@ -360,49 +391,12 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint
return 0, false, err return 0, false, err
} }
weight := standard.Weight return standard.FcrNumber, true, nil
if weight > 10 {
return weight / 1000, true, nil
}
return weight, true, nil
} }
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
if projectFlockID == 0 { // Body-weight tracking is removed; keep stub for report compatibility.
return 0, 0, nil return 0, 0, nil
}
totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
actualQty := totalChickinQty - totalDepletion
avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
return 0, 0, err
}
totalWeight = actualQty * avgWeight
return totalWeight, actualQty, nil
}
func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64
err := r.DB().WithContext(ctx).
Table("project_chickins").
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Scan(&result).Error
return result, err
} }
func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
@@ -418,16 +412,8 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.
} }
func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
var result float64 // Body-weight tracking is removed; keep stub for report compatibility.
err := r.DB().WithContext(ctx). return 0, nil
Table("recording_bws").
Select("COALESCE(AVG(recording_bws.avg_weight), 0)").
Joins("JOIN recordings ON recordings.id = recording_bws.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID).
Scan(&result).Error
return result, err
} }
func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
@@ -9,6 +9,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rProjectFlock "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"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
@@ -121,6 +122,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
if err := s.attachLatestApprovals(c.Context(), recordings); err != nil { if err := s.attachLatestApprovals(c.Context(), recordings); err != nil {
return nil, 0, err return nil, 0, err
} }
if err := s.attachProductionStandards(c.Context(), recordings); err != nil {
return nil, 0, err
}
return recordings, total, nil return recordings, total, nil
} }
@@ -138,6 +142,9 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
if err := s.attachLatestApproval(c.Context(), recording); err != nil { if err := s.attachLatestApproval(c.Context(), recording); err != nil {
return nil, err return nil, err
} }
if err := s.attachProductionStandard(c.Context(), recording); err != nil {
return nil, err
}
return recording, nil return recording, nil
} }
@@ -233,12 +240,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err return err
} }
mappedBodyWeights := recordingutil.MapBodyWeights(createdRecording.Id, req.BodyWeights)
if err := s.Repository.CreateBodyWeights(tx, mappedBodyWeights); err != nil {
s.Log.Errorf("Failed to persist body weights: %+v", err)
return err
}
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks) mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
s.Log.Errorf("Failed to persist stocks: %+v", err) s.Log.Errorf("Failed to persist stocks: %+v", err)
@@ -261,7 +262,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err return err
} }
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err) s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err return err
} }
@@ -291,7 +292,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return nil, err return nil, err
} }
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -311,12 +312,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
recordingEntity = recording recordingEntity = recording
hasBodyChanges := req.BodyWeights != nil
hasStockChanges := req.Stocks != nil hasStockChanges := req.Stocks != nil
hasDepletionChanges := req.Depletions != nil hasDepletionChanges := req.Depletions != nil
hasEggChanges := req.Eggs != nil hasEggChanges := req.Eggs != nil
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges { if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
return nil return nil
} }
@@ -346,17 +346,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
} }
if hasBodyChanges {
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear body weights: %+v", err)
return err
}
if err := s.Repository.CreateBodyWeights(tx, recordingutil.MapBodyWeights(recordingEntity.Id, req.BodyWeights)); err != nil {
s.Log.Errorf("Failed to update body weights: %+v", err)
return err
}
}
if hasStockChanges { if hasStockChanges {
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil { if err != nil {
@@ -402,7 +391,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil, nil, nil)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err) s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
return err return err
} }
@@ -426,13 +415,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, nil, nil, existingEggs, mappedEggs)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, mappedEggs)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
return err return err
} }
} }
if hasBodyChanges || hasStockChanges || hasDepletionChanges { if hasStockChanges || hasDepletionChanges {
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err) s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err return err
@@ -596,7 +585,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldEggs, nil)); err != nil {
return err return err
} }
@@ -724,7 +713,6 @@ func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.
func buildWarehouseDeltas( func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion, oldDepletions, newDepletions []entity.RecordingDepletion,
oldStocks, newStocks []entity.RecordingStock,
oldEggs, newEggs []entity.RecordingEgg, oldEggs, newEggs []entity.RecordingEgg,
) map[uint]float64 { ) map[uint]float64 {
deltas := make(map[uint]float64) deltas := make(map[uint]float64)
@@ -775,7 +763,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var prevCumDepletionQty float64 var prevCumDepletionQty float64
var prevCumIntake float64 var prevCumIntake float64
var prevAvgWeight float64
if prevRecording != nil { if prevRecording != nil {
if prevRecording.TotalDepletionQty != nil { if prevRecording.TotalDepletionQty != nil {
prevCumDepletionQty = *prevRecording.TotalDepletionQty prevCumDepletionQty = *prevRecording.TotalDepletionQty
@@ -783,10 +770,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
if prevRecording.CumIntake != nil { if prevRecording.CumIntake != nil {
prevCumIntake = float64(*prevRecording.CumIntake) prevCumIntake = float64(*prevRecording.CumIntake)
} }
prevAvgWeight, err = s.Repository.GetAverageBodyWeight(tx, prevRecording.Id)
if err != nil {
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
}
} }
totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId) totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
@@ -794,20 +777,25 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
return fmt.Errorf("getTotalChick: %w", err) return fmt.Errorf("getTotalChick: %w", err)
} }
currentAvgWeight, err := s.Repository.GetAverageBodyWeight(tx, recording.Id)
if err != nil {
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
}
usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id) usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
if err != nil { if err != nil {
return fmt.Errorf("getFeedUsageInGrams: %w", err) return fmt.Errorf("getFeedUsageInGrams: %w", err)
} }
currentAvgGrams := recordingutil.ToGrams(currentAvgWeight) totalEggQty, totalEggWeightGrams, err := s.Repository.GetEggSummaryByRecording(tx, recording.Id)
currentAvgKg := recordingutil.GramsToKg(currentAvgGrams) if err != nil {
prevAvgGrams := recordingutil.ToGrams(prevAvgWeight) return fmt.Errorf("getEggSummaryByRecording: %w", err)
prevAvgKg := recordingutil.GramsToKg(prevAvgGrams) }
cumulativeEggQty, err := s.Repository.GetCumulativeEggQtyByProjectFlockKandang(tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
if err != nil {
return fmt.Errorf("getCumulativeEggQtyByProjectFlockKandang: %w", err)
}
initialChickin, err := s.Repository.GetTotalChickinByProjectFlockKandang(tx, recording.ProjectFlockKandangId)
if err != nil {
return fmt.Errorf("getTotalChickinByProjectFlockKandang: %w", err)
}
currentDepletion := float64(totalDepletionQty) currentDepletion := float64(totalDepletionQty)
cumDepletionQty := prevCumDepletionQty + currentDepletion cumDepletionQty := prevCumDepletionQty + currentDepletion
@@ -840,24 +828,64 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
recording.CumDepletionRate = nil recording.CumDepletionRate = nil
} }
if currentAvgGrams > 0 && prevAvgGrams > 0 { var feedIntake float64
dailyGainKg := (currentAvgGrams - prevAvgGrams) / 1000 if remainingChick > 0 && usageInGrams > 0 {
updates["daily_gain"] = dailyGainKg feedIntake = (usageInGrams / remainingChick) * 1000
recording.DailyGain = &dailyGainKg updates["feed_intake"] = feedIntake
recording.FeedIntake = &feedIntake
} else { } else {
dailyGainKg := 0.0 updates["feed_intake"] = gorm.Expr("NULL")
updates["daily_gain"] = dailyGainKg recording.FeedIntake = nil
recording.DailyGain = &dailyGainKg
} }
if currentAvgKg > 0 && remainingChick > 0 { var handDay float64
avgDailyGain := (currentAvgKg - prevAvgKg) / remainingChick if remainingChick > 0 && totalEggQty >= 0 {
updates["avg_daily_gain"] = avgDailyGain handDay = (totalEggQty / remainingChick) * 100
recording.AvgDailyGain = &avgDailyGain updates["hand_day"] = handDay
recording.HandDay = &handDay
} else { } else {
avgDailyGain := 0.0 updates["hand_day"] = gorm.Expr("NULL")
updates["avg_daily_gain"] = avgDailyGain recording.HandDay = nil
recording.AvgDailyGain = &avgDailyGain }
var handHouse float64
if initialChickin > 0 && cumulativeEggQty >= 0 {
handHouse = cumulativeEggQty / initialChickin
updates["hand_house"] = handHouse
recording.HandHouse = &handHouse
} else {
updates["hand_house"] = gorm.Expr("NULL")
recording.HandHouse = nil
}
var eggMesh float64
if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMesh = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mesh"] = eggMesh
recording.EggMesh = &eggMesh
} else {
updates["egg_mesh"] = gorm.Expr("NULL")
recording.EggMesh = nil
}
var eggWeight float64
if totalEggQty > 0 && totalEggWeightGrams > 0 {
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
updates["egg_weight"] = eggWeight
recording.EggWeight = &eggWeight
} else {
updates["egg_weight"] = gorm.Expr("NULL")
recording.EggWeight = nil
}
var fcrValue float64
if usageInGrams > 0 && totalEggWeightGrams > 0 {
fcrValue = totalEggWeightGrams / usageInGrams
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
updates["fcr_value"] = gorm.Expr("NULL")
recording.FcrValue = nil
} }
if usageInGrams > 0 && totalChick > 0 { if usageInGrams > 0 && totalChick > 0 {
@@ -882,16 +910,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
recording.CumIntake = nil recording.CumIntake = nil
} }
if usageInGrams > 0 && currentAvgKg > 0 {
feedUsageKg := usageInGrams / 1000
fcrValue := feedUsageKg / currentAvgKg
updates["fcr_value"] = fcrValue
recording.FcrValue = &fcrValue
} else {
updates["fcr_value"] = gorm.Expr("NULL")
recording.FcrValue = nil
}
if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil { if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil {
return err return err
} }
@@ -997,6 +1015,104 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit
return nil return nil
} }
type productionStandardValues struct {
HandDay *float64
HandHouse *float64
FeedIntake *float64
MaxDepletion *float64
EggMesh *float64
EggWeight *float64
}
func (s *recordingService) attachProductionStandards(ctx context.Context, items []entity.Recording) error {
if len(items) == 0 {
return nil
}
for i := range items {
if err := s.attachProductionStandard(ctx, &items[i]); err != nil {
s.Log.Warnf("Unable to load production standard for recording %d: %+v", items[i].Id, err)
}
}
return nil
}
func (s *recordingService) attachProductionStandard(ctx context.Context, item *entity.Recording) error {
if item == nil || item.Id == 0 {
return nil
}
if item.Day == nil || *item.Day <= 0 {
return nil
}
if item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
return nil
}
standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId
if standardID == 0 {
return nil
}
week := ((int(*item.Day) - 1) / 7) + 1
if week <= 0 {
return nil
}
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
db := s.Repository.DB()
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
var standard productionStandardValues
var standardFcr *float64
if category == string(utils.ProjectFlockCategoryLaying) {
detail, err := standardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if detail != nil {
standard.HandDay = detail.TargetHenDayProduction
standard.HandHouse = detail.TargetHenHouseProduction
standard.EggWeight = detail.TargetEggWeight
standard.EggMesh = detail.TargetEggMass
}
}
growthDetail, err := growthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if growthDetail != nil {
standard.FeedIntake = growthDetail.FeedIntake
standard.MaxDepletion = growthDetail.MaxDepletion
if category == string(utils.ProjectFlockCategoryLaying) && growthDetail.TargetMeanBw != nil && item.ProjectFlockKandang.ProjectFlock.FcrId > 0 {
targetWeight := *growthDetail.TargetMeanBw
if targetWeight > 10 {
targetWeight = targetWeight / 1000
}
if targetWeight > 0 {
fcrStd, ok, err := s.Repository.GetFcrStandardNumber(db, item.ProjectFlockKandang.ProjectFlock.FcrId, targetWeight)
if err != nil {
return err
}
if ok {
standardFcr = &fcrStd
}
}
}
}
item.StandardHandDay = standard.HandDay
item.StandardHandHouse = standard.HandHouse
item.StandardFeedIntake = standard.FeedIntake
item.StandardMaxDepletion = standard.MaxDepletion
item.StandardEggMesh = standard.EggMesh
item.StandardEggWeight = standard.EggWeight
item.StandardFcr = standardFcr
return nil
}
func uniqueUintSlice(values []uint) []uint { func uniqueUintSlice(values []uint) []uint {
if len(values) == 0 { if len(values) == 0 {
return nil return nil
@@ -1,12 +1,6 @@
package validation package validation
type ( type (
BodyWeight struct {
AvgWeight float64 `json:"avg_weight" validate:"required"`
Qty float64 `json:"qty" validate:"required,gt=0"`
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
}
Stock struct { Stock struct {
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
Qty float64 `json:"qty" validate:"required,gte=0"` Qty float64 `json:"qty" validate:"required,gte=0"`
@@ -27,14 +21,12 @@ type (
type Create struct { type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
BodyWeights []BodyWeight `json:"body_weights" validate:"dive"`
Stocks []Stock `json:"stocks" validate:"dive"` Stocks []Stock `json:"stocks" validate:"dive"`
Depletions []Depletion `json:"depletions" validate:"dive"` Depletions []Depletion `json:"depletions" validate:"dive"`
Eggs []Egg `json:"eggs" validate:"omitempty,dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
} }
type Update struct { type Update struct {
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"` Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
+4 -2
View File
@@ -8,10 +8,11 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs" uniformitys "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -24,8 +25,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
chickins.ChickinModule{}, chickins.ChickinModule{},
transferLayings.TransferLayingModule{}, transferLayings.TransferLayingModule{},
projectFlockKandangs.ProjectFlockKandangModule{}, projectFlockKandangs.ProjectFlockKandangModule{},
uniformitys.UniformityModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
for _, m := range allModules { for _, m := range allModules {
m.RegisterRoutes(group, db, validate) m.RegisterRoutes(group, db, validate)
@@ -0,0 +1,292 @@
package controller
import (
"math"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
type UniformityController struct {
UniformityService service.UniformityService
}
func NewUniformityController(uniformityService service.UniformityService) *UniformityController {
return &UniformityController{
UniformityService: uniformityService,
}
}
func (u *UniformityController) GetAll(c *fiber.Ctx) error {
query, err := validation.ParseQuery(c)
if err != nil {
return err
}
result, totalResults, err := u.UniformityService.GetAll(c, query)
if err != nil {
return err
}
standards, err := u.UniformityService.MapStandards(c, result)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all production uniformities successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
Filters: fiber.Map{
"location_id": "",
"project_flock_id": "",
"status": "Pengajuan",
},
},
Data: dto.ToUniformityListDTOsWithStandard(result, standards),
})
}
func (u *UniformityController) GetOne(c *fiber.Ctx) error {
id, err := validation.ParseIDParam(c, "id")
if err != nil {
return err
}
result, err := u.UniformityService.GetOne(c, id)
if err != nil {
return err
}
withDetails := c.QueryBool("with_details", false)
calculation := service.UniformityCalculation{}
var document *entity.Document
var meanWeight float64
if result.MeanUp > 0 {
meanWeight = math.Round(result.MeanUp / 1.10)
}
if withDetails {
var err error
calculation, document, err = u.UniformityService.CalculateUniformityFromDocument(c, id)
if err != nil {
return err
}
} else {
calculation = service.UniformityCalculation{
ChickQtyOfWeight: result.ChickQtyOfWeight,
MeanWeight: meanWeight,
MeanDown: result.MeanDown,
MeanUp: result.MeanUp,
UniformQty: result.UniformQty,
OutsideQty: result.NotUniformQty,
Uniformity: result.Uniformity,
Cv: result.Cv,
}
}
standard, err := u.UniformityService.GetStandard(c, result)
if err != nil {
return err
}
var standardDTO *dto.UniformityStandardDTO
if standard != nil {
standardDTO = &dto.UniformityStandardDTO{
MeanWeight: standard.MeanWeight,
Uniformity: standard.Uniformity,
}
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get production uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO),
})
}
func (u *UniformityController) CreateOne(c *fiber.Ctx) error {
req, file, err := validation.ParseCreate(c)
if err != nil {
return err
}
rows, err := u.UniformityService.ParseBodyWeightExcel(c, file)
if err != nil {
return err
}
calculation, err := u.UniformityService.ComputeUniformity(rows)
if err != nil {
return err
}
result, err := u.UniformityService.CreateOne(c, req, file, rows)
if err != nil {
return err
}
document := dto.NewDocumentForResponse(file.Filename)
standard, err := u.UniformityService.GetStandard(c, result)
if err != nil {
return err
}
var standardDTO *dto.UniformityStandardDTO
if standard != nil {
standardDTO = &dto.UniformityStandardDTO{
MeanWeight: standard.MeanWeight,
Uniformity: standard.Uniformity,
}
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO),
})
}
func (u *UniformityController) UploadBodyWeightExcel(c *fiber.Ctx) error {
files, err := validation.ParseUploadFiles(c)
if err != nil {
return err
}
rows, err := u.UniformityService.ParseBodyWeightExcel(c, files[0])
if err != nil {
return err
}
calculation, err := u.UniformityService.ComputeUniformity(rows)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Uniformity verified successfully",
Data: dto.ToUniformityVerificationDTO(calculation),
})
}
func (u *UniformityController) UpdateOne(c *fiber.Ctx) error {
id, err := validation.ParseIDParam(c, "id")
if err != nil {
return err
}
req, file, err := validation.ParseUpdate(c)
if err != nil {
return err
}
var rows []service.BodyWeightExcelRow
if file != nil {
parsed, err := u.UniformityService.ParseBodyWeightExcel(c, file)
if err != nil {
return err
}
rows = parsed
}
result, err := u.UniformityService.UpdateOne(c, req, id, file, rows)
if err != nil {
return err
}
standard, err := u.UniformityService.GetStandard(c, result)
if err != nil {
return err
}
var standardDTO *dto.UniformityStandardDTO
if standard != nil {
standardDTO = &dto.UniformityStandardDTO{
MeanWeight: standard.MeanWeight,
Uniformity: standard.Uniformity,
}
}
calculation := service.UniformityCalculation{
ChickQtyOfWeight: result.ChickQtyOfWeight,
MeanWeight: math.Round(result.MeanUp / 1.10),
MeanDown: result.MeanDown,
MeanUp: result.MeanUp,
UniformQty: result.UniformQty,
OutsideQty: result.NotUniformQty,
Uniformity: result.Uniformity,
Cv: result.Cv,
}
var document *entity.Document
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update uniformity successfully",
Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO),
})
}
func (u *UniformityController) DeleteOne(c *fiber.Ctx) error {
id, err := validation.ParseIDParam(c, "id")
if err != nil {
return err
}
if err := u.UniformityService.DeleteOne(c, id); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete uniformity successfully",
})
}
func (u *UniformityController) Approve(c *fiber.Ctx) error {
req, err := validation.ParseApprove(c)
if err != nil {
return err
}
results, err := u.UniformityService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit uniformity approvals successfully"
)
if len(results) == 1 {
message = "Submit uniformity approval successfully"
data = dto.ToUniformityListDTOs(results)[0]
} else {
data = dto.ToUniformityListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
@@ -0,0 +1,236 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
)
type UniformitySamplingDTO struct {
ChickQtyOfWeight float64 `json:"chick_qty_of_weight"`
MeanWeight float64 `json:"mean_weight"`
MeanDown float64 `json:"mean_down"`
MeanUp float64 `json:"mean_up"`
}
type UniformityResultDTO struct {
UniformQty float64 `json:"uniform_qty"`
OutsideQty float64 `json:"outside_qty"`
Uniformity float64 `json:"uniformity"`
Cv float64 `json:"cv"`
}
type UniformityStandardDTO struct {
MeanWeight *float64 `json:"mean_weight"`
Uniformity *float64 `json:"uniformity"`
}
type UniformityDetailItemDTO struct {
Id int `json:"id"`
Weight float64 `json:"weight"`
Range string `json:"range"`
}
type UniformityVerificationDTO struct {
Sampling UniformitySamplingDTO `json:"sampling"`
Result UniformityResultDTO `json:"result"`
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
}
type UniformityInfoDTO struct {
Tanggal string `json:"tanggal"`
LokasiFarm string `json:"lokasi_farm"`
ProjectFlock string `json:"project_flock"`
Kandang string `json:"kandang"`
FileName string `json:"file_name"`
}
type UniformityDetailDTO struct {
Id uint `json:"id"`
InfoUmum UniformityInfoDTO `json:"info_umum"`
Sampling UniformitySamplingDTO `json:"sampling"`
Result UniformityResultDTO `json:"result"`
Standard *UniformityStandardDTO `json:"standard"`
UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"`
}
type UniformityListDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
LocationName string `json:"location_name"`
FlockName string `json:"flock_name"`
KandangName string `json:"kandang_name"`
AppliedAt *time.Time `json:"applied_at"`
Week int `json:"week"`
Status string `json:"status"`
Uniformity float64 `json:"uniformity"`
Cv float64 `json:"cv"`
ChickQtyOfWeight float64 `json:"chick_qty_of_weight"`
UniformQty float64 `json:"uniform_qty"`
MeanUp float64 `json:"mean_up"`
MeanDown float64 `json:"mean_down"`
StandardMeanWeight *float64 `json:"standard_mean_weight"`
StandardUniformity *float64 `json:"standard_uniformity"`
CreatedAt time.Time `json:"created_at"`
CreatedBy uint `json:"created_by"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
func NewDocumentForResponse(name string) *entity.Document {
if name == "" {
return nil
}
return &entity.Document{Name: name}
}
func ToUniformityVerificationDTO(calc service.UniformityCalculation) UniformityVerificationDTO {
return UniformityVerificationDTO{
Sampling: toUniformitySamplingDTO(calc),
Result: toUniformityResultDTO(calc),
UniformityDetails: toUniformityDetailItemsDTO(calc),
}
}
func ToUniformityDetailDTO(
entityData entity.ProjectFlockKandangUniformity,
calc service.UniformityCalculation,
document *entity.Document,
standard *UniformityStandardDTO,
) UniformityDetailDTO {
info := UniformityInfoDTO{
Tanggal: formatUniformityDate(entityData.UniformDate),
LokasiFarm: resolveLocationName(entityData.ProjectFlockKandang),
ProjectFlock: resolveProjectFlockName(entityData.ProjectFlockKandang),
Kandang: resolveKandangName(entityData.ProjectFlockKandang),
FileName: "",
}
if document != nil {
info.FileName = document.Name
}
return UniformityDetailDTO{
Id: entityData.Id,
InfoUmum: info,
Sampling: toUniformitySamplingDTO(calc),
Result: toUniformityResultDTO(calc),
Standard: standard,
UniformityDetails: toUniformityDetailItemsDTO(calc),
}
}
func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []UniformityListDTO {
result := make([]UniformityListDTO, len(items))
for i, item := range items {
var latestApproval *approvalDTO.ApprovalRelationDTO
status := "Pengajuan"
if item.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*item.LatestApproval)
latestApproval = &mapped
if mapped.StepName != "" {
status = mapped.StepName
}
}
result[i] = UniformityListDTO{
Id: item.Id,
ProjectFlockKandangId: item.ProjectFlockKandangId,
LocationName: resolveLocationName(item.ProjectFlockKandang),
FlockName: resolveProjectFlockName(item.ProjectFlockKandang),
KandangName: resolveKandangName(item.ProjectFlockKandang),
AppliedAt: item.UniformDate,
Week: item.Week,
Status: status,
Uniformity: item.Uniformity,
Cv: item.Cv,
ChickQtyOfWeight: item.ChickQtyOfWeight,
UniformQty: item.UniformQty,
MeanUp: item.MeanUp,
MeanDown: item.MeanDown,
CreatedAt: item.CreatedAt,
CreatedBy: item.CreatedBy,
LatestApproval: latestApproval,
}
}
return result
}
func ToUniformityListDTOsWithStandard(
items []entity.ProjectFlockKandangUniformity,
standards map[uint]service.UniformityStandard,
) []UniformityListDTO {
result := ToUniformityListDTOs(items)
if len(result) == 0 || len(standards) == 0 {
return result
}
for i := range result {
if std, ok := standards[result[i].Id]; ok {
result[i].StandardMeanWeight = std.MeanWeight
result[i].StandardUniformity = std.Uniformity
}
}
return result
}
func toUniformitySamplingDTO(calc service.UniformityCalculation) UniformitySamplingDTO {
return UniformitySamplingDTO{
ChickQtyOfWeight: calc.ChickQtyOfWeight,
MeanWeight: calc.MeanWeight,
MeanDown: calc.MeanDown,
MeanUp: calc.MeanUp,
}
}
func toUniformityResultDTO(calc service.UniformityCalculation) UniformityResultDTO {
return UniformityResultDTO{
UniformQty: calc.UniformQty,
OutsideQty: calc.OutsideQty,
Uniformity: calc.Uniformity,
Cv: calc.Cv,
}
}
func toUniformityDetailItemsDTO(calc service.UniformityCalculation) []UniformityDetailItemDTO {
result := make([]UniformityDetailItemDTO, len(calc.Details))
for i, item := range calc.Details {
result[i] = UniformityDetailItemDTO{
Id: item.Id,
Weight: item.Weight,
Range: item.Range,
}
}
return result
}
func resolveLocationName(pfk entity.ProjectFlockKandang) string {
if pfk.Kandang.Id != 0 && pfk.Kandang.Location.Id != 0 {
return pfk.Kandang.Location.Name
}
if pfk.ProjectFlock.Id != 0 && pfk.ProjectFlock.Location.Id != 0 {
return pfk.ProjectFlock.Location.Name
}
return ""
}
func resolveProjectFlockName(pfk entity.ProjectFlockKandang) string {
if pfk.ProjectFlock.Id != 0 {
return pfk.ProjectFlock.FlockName
}
return ""
}
func resolveKandangName(pfk entity.ProjectFlockKandang) string {
if pfk.Kandang.Id != 0 {
return pfk.Kandang.Name
}
return ""
}
func formatUniformityDate(date *time.Time) string {
if date == nil || date.IsZero() {
return ""
}
return date.Format("2006-01-02")
}
@@ -0,0 +1,57 @@
package uniformitys
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
"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"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
sUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type UniformityModule struct{}
func (UniformityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
uniformityRepo := rUniformity.NewUniformityRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
userRepo := rUser.NewUserRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowUniformity, utils.UniformityApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register uniformity approval workflow: %v", err))
}
uniformityService := sUniformity.NewUniformityService(
uniformityRepo,
documentSvc,
approvalRepo,
approvalSvc,
projectFlockKandangRepo,
productionStandardRepo,
standardGrowthDetailRepo,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
UniformityRoutes(router, userService, uniformityService)
}
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type UniformityRepository interface {
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
}
type UniformityRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectFlockKandangUniformity]
}
func NewUniformityRepository(db *gorm.DB) UniformityRepository {
return &UniformityRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
}
}
@@ -0,0 +1,25 @@
package uniformitys
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/controllers"
uniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func UniformityRoutes(v1 fiber.Router, u user.UserService, s uniformity.UniformityService) {
ctrl := controller.NewUniformityController(s)
route := v1.Group("/uniformities")
route.Use(m.Auth(u))
route.Get("/", m.RequirePermissions(m.P_Uniformities_GetAll), ctrl.GetAll)
route.Post("/", m.RequirePermissions(m.P_Uniformities_CreateOne), ctrl.CreateOne)
route.Post("/verify", m.RequirePermissions(m.P_Uniformities_Verify), ctrl.UploadBodyWeightExcel)
route.Post("/approvals", m.RequirePermissions(m.P_Uniformities_Approval), ctrl.Approve)
route.Get("/:id", m.RequirePermissions(m.P_Uniformities_GetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_Uniformities_UpdateOne), ctrl.UpdateOne)
route.Delete("/:id", m.RequirePermissions(m.P_Uniformities_DeleteOne), ctrl.DeleteOne)
}
@@ -0,0 +1,200 @@
package service
import (
"io"
"mime/multipart"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
)
type BodyWeightExcelRow struct {
No int `json:"no"`
Weight float64 `json:"weight"`
Range string `json:"range,omitempty"`
}
func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
if file == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
}
reader, err := file.Open()
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
}
defer reader.Close()
rows, err := parseBodyWeightExcelReader(reader)
if err != nil {
return nil, err
}
return rows, nil
}
func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
xlsx, err := excelize.OpenReader(reader)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
}
defer func() {
_ = xlsx.Close()
}()
sheets := xlsx.GetSheetList()
if len(sheets) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
}
sheetName := sheets[0]
if len(sheets) > 1 {
sheetName = sheets[1]
}
rows, err := xlsx.GetRows(sheetName, excelize.Options{RawCellValue: true})
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
}
return parseBodyWeightRows(rows)
}
func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
if headerRowIdx < 0 || bwCol < 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
}
result := make([]BodyWeightExcelRow, 0)
lastNo := 0
for i := headerRowIdx + 1; i < len(rows); i++ {
row := rows[i]
weightStr := cellAt(row, bwCol)
weightVal, ok := parseNumber(weightStr)
if !ok {
continue
}
noVal := 0
if noCol >= 0 {
if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
noVal = int(parsed)
}
}
if noVal <= 0 {
noVal = lastNo + 1
}
if noVal > lastNo {
lastNo = noVal
}
rangeVal := ""
if rangeCol >= 0 {
rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
}
rowPayload := BodyWeightExcelRow{
No: noVal,
Weight: weightVal,
Range: rangeVal,
}
if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
}
result = append(result, rowPayload)
}
if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
}
return result, nil
}
func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
rowIdx = -1
noCol = -1
bwCol = -1
rangeCol = -1
for i, row := range rows {
tempNo := -1
tempBW := -1
tempRange := -1
for j, cell := range row {
label := normalizeHeader(cell)
switch label {
case "no":
tempNo = j
case "bw":
tempBW = j
case "outsiderange":
tempRange = j
default:
if strings.HasPrefix(label, "bw") {
tempBW = j
} else if strings.HasPrefix(label, "no") {
tempNo = j
} else if strings.Contains(label, "range") {
tempRange = j
}
}
}
if tempBW >= 0 {
rowIdx = i
bwCol = tempBW
noCol = tempNo
rangeCol = tempRange
break
}
}
return rowIdx, noCol, bwCol, rangeCol
}
func cellAt(row []string, idx int) string {
if idx < 0 || idx >= len(row) {
return ""
}
return strings.TrimSpace(row[idx])
}
func normalizeHeader(value string) string {
trimmed := strings.ToLower(strings.TrimSpace(value))
if trimmed == "" {
return ""
}
var b strings.Builder
for _, r := range trimmed {
if r >= 'a' && r <= 'z' {
b.WriteRune(r)
}
}
return b.String()
}
func parseNumber(value string) (float64, bool) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return 0, false
}
if strings.Contains(trimmed, ",") {
if strings.Contains(trimmed, ".") {
trimmed = strings.ReplaceAll(trimmed, ",", "")
} else {
trimmed = strings.ReplaceAll(trimmed, ",", ".")
}
}
parsed, err := strconv.ParseFloat(trimmed, 64)
if err != nil {
return 0, false
}
return parsed, true
}
@@ -0,0 +1,959 @@
package service
import (
"context"
"errors"
"fmt"
"math"
"mime/multipart"
"net/http"
"strings"
"time"
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"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
"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"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type UniformityService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error)
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error)
}
type uniformityService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.UniformityRepository
DocumentSvc commonSvc.DocumentService
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
ProjectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProductionStandardRepo rProductionStandard.ProductionStandardRepository
StandardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository
}
func NewUniformityService(
repo repository.UniformityRepository,
documentSvc commonSvc.DocumentService,
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
projectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository,
productionStandardRepo rProductionStandard.ProductionStandardRepository,
standardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository,
validate *validator.Validate,
) UniformityService {
return &uniformityService{
Log: utils.Log,
Validate: validate,
Repository: repo,
DocumentSvc: documentSvc,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
ProjectFlockKandangRepo: projectFlockKandangRepo,
ProductionStandardRepo: productionStandardRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
}
}
func (s uniformityService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("ProjectFlockKandang.ProjectFlock.Location").
Preload("ProjectFlockKandang.Kandang.Location")
}
func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
uniformitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId)
}
if params.Week != 0 {
db = db.Where("week = ?", params.Week)
}
return db.Order("uniform_date DESC").Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get uniformitys: %+v", err)
return nil, 0, err
}
if err := s.attachLatestApprovals(c.Context(), uniformitys); err != nil {
return nil, 0, err
}
return uniformitys, total, nil
}
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
if err != nil {
s.Log.Errorf("Failed get uniformity by id: %+v", err)
return nil, err
}
if err := s.attachLatestApproval(c.Context(), uniformity); err != nil {
return nil, err
}
return uniformity, nil
}
func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
return s.GetOne(c, id)
}
func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
if uniformity == nil {
return nil, nil
}
return s.resolveUniformityStandard(c.Context(), *uniformity)
}
func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error) {
if len(items) == 0 {
return nil, nil
}
if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
return nil, nil
}
categoryStandard := make(map[string]*entity.ProductionStandard)
detailCache := make(map[uint]map[int]entity.StandardGrowthDetail)
result := make(map[uint]UniformityStandard, len(items))
for _, item := range items {
if item.Id == 0 {
continue
}
standard, err := s.resolveCategoryStandard(c.Context(), item.ProjectFlockKandang.ProjectFlock.Category, categoryStandard)
if err != nil {
return nil, err
}
if standard == nil {
continue
}
weekMap, ok := detailCache[standard.Id]
if !ok {
details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(c.Context(), standard.Id)
if err != nil {
return nil, err
}
weekMap = make(map[int]entity.StandardGrowthDetail, len(details))
for _, detail := range details {
weekMap[detail.Week] = detail
}
detailCache[standard.Id] = weekMap
}
detail, ok := weekMap[item.Week]
if !ok {
continue
}
standardDTO := UniformityStandard{
MeanWeight: cloneFloat64(detail.TargetMeanBw),
Uniformity: float64Ptr(detail.MinUniformity),
}
result[item.Id] = standardDTO
}
return result, nil
}
func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if s.ProjectFlockKandangRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available")
}
if file == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
}
uniformDate, err := time.Parse("2006-01-02", req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
}
if err := commonSvc.EnsureRelations(
c.Context(),
commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: &req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil {
return nil, err
}
if len(rows) == 0 {
parsedRows, err := s.ParseBodyWeightExcel(c, file)
if err != nil {
return nil, err
}
rows = parsedRows
}
calculation, err := s.ComputeUniformity(rows)
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.ProjectFlockKandangUniformity{
Uniformity: calculation.Uniformity,
Week: req.Week,
Cv: calculation.Cv,
ChickQtyOfWeight: calculation.ChickQtyOfWeight,
MeanUp: calculation.MeanUp,
MeanDown: calculation.MeanDown,
ProjectFlockKandangId: req.ProjectFlockKandangId,
UniformQty: calculation.UniformQty,
NotUniformQty: calculation.OutsideQty,
UniformDate: &uniformDate,
CreatedBy: actorID,
}
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if err := s.createUniformityApproval(
c.Context(),
tx,
createBody.Id,
utils.UniformityStepPengajuan,
entity.ApprovalActionCreated,
actorID,
nil,
); err != nil {
return err
}
return nil
}); err != nil {
s.Log.Errorf("Failed to create uniformity: %+v", err)
return nil, err
}
if s.DocumentSvc != nil {
actorIDCopy := actorID
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: "UNIFORMITY",
DocumentableID: uint64(createBody.Id),
CreatedBy: &actorIDCopy,
Files: []commonSvc.DocumentFile{
{
File: file,
Type: "UNIFORMITY",
},
},
})
if err != nil {
s.rollbackUniformityCreate(c.Context(), createBody.Id)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
}
}
return s.GetOne(c, createBody.Id)
}
func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
var uniformDate *time.Time
if req.Date != nil {
parsed, err := time.Parse("2006-01-02", *req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
}
updateBody["uniform_date"] = parsed
uniformDate = &parsed
}
if req.ProjectFlockKandangId != nil {
if s.ProjectFlockKandangRepo == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available")
}
if err := commonSvc.EnsureRelations(
c.Context(),
commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
updateBody["project_flock_kandang_id"] = *req.ProjectFlockKandangId
}
if req.Week != nil {
updateBody["week"] = *req.Week
}
if req.Date != nil || req.ProjectFlockKandangId != nil || req.Week != nil {
current, err := s.Repository.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
return nil, err
}
targetDate := uniformDate
if targetDate == nil {
targetDate = current.UniformDate
}
targetWeek := current.Week
if req.Week != nil {
targetWeek = *req.Week
}
targetPFKID := current.ProjectFlockKandangId
if req.ProjectFlockKandangId != nil {
targetPFKID = *req.ProjectFlockKandangId
}
if targetDate != nil {
if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil {
return nil, err
}
}
}
if file != nil {
if s.DocumentSvc == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
if len(rows) == 0 {
parsedRows, err := s.ParseBodyWeightExcel(c, file)
if err != nil {
return nil, err
}
rows = parsedRows
}
calculation, err := s.ComputeUniformity(rows)
if err != nil {
return nil, err
}
updateBody["uniformity"] = calculation.Uniformity
updateBody["cv"] = calculation.Cv
updateBody["chick_qty_of_weight"] = calculation.ChickQtyOfWeight
updateBody["mean_up"] = calculation.MeanUp
updateBody["mean_down"] = calculation.MeanDown
updateBody["uniform_qty"] = calculation.UniformQty
updateBody["not_uniform_qty"] = calculation.OutsideQty
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if file == nil {
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to update uniformity: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
existingDocs, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(id))
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
actorIDCopy := actorID
uploadResults, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: "UNIFORMITY",
DocumentableID: uint64(id),
CreatedBy: &actorIDCopy,
Files: []commonSvc.DocumentFile{
{
File: file,
Type: "UNIFORMITY",
},
},
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if len(uploadResults) > 0 {
ids := make([]uint, 0, len(uploadResults))
for _, result := range uploadResults {
if result.Document.Id != 0 {
ids = append(ids, result.Document.Id)
}
}
if len(ids) > 0 {
_ = s.DocumentSvc.DeleteDocuments(c.Context(), ids, true)
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to update uniformity: %+v", err)
return nil, err
}
if len(existingDocs) > 0 {
oldIDs := make([]uint, 0, len(existingDocs))
for _, doc := range existingDocs {
if doc.Id != 0 {
oldIDs = append(oldIDs, doc.Id)
}
}
if len(oldIDs) > 0 {
_ = s.DocumentSvc.DeleteDocuments(c.Context(), oldIDs, true)
}
}
return s.GetOne(c, id)
}
func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error {
if projectFlockKandangID == 0 || week == 0 || uniformDate == nil || uniformDate.IsZero() {
return nil
}
query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandangUniformity{}).
Where("project_flock_kandang_id = ? AND week = ? AND uniform_date = ?", projectFlockKandangID, week, *uniformDate)
if id != 0 {
query = query.Where("id <> ?", id)
}
var count int64
if err := query.Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness")
}
if count > 0 {
return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang, week, and date")
}
return nil
}
func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to delete uniformity: %+v", err)
return err
}
return nil
}
func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actionValue := strings.ToUpper(strings.TrimSpace(req.Action))
var action entity.ApprovalAction
switch actionValue {
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
ids := uniqueUintSlice(req.ApprovableIds)
if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.UniformityStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.UniformityStepDisetujui
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
ctx := c.Context()
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
for _, id := range ids {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Uniformity %d not found", id))
}
return err
}
if _, err := approvalSvc.CreateApproval(
ctx,
utils.ApprovalWorkflowUniformity,
id,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
}
return nil
})
if transactionErr != nil {
if fiberErr, ok := transactionErr.(*fiber.Error); ok {
return nil, fiberErr
}
s.Log.Errorf("Failed to record approvals for uniformities %+v: %+v", ids, transactionErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit uniformity approval")
}
results := make([]entity.ProjectFlockKandangUniformity, 0, len(ids))
for _, id := range ids {
loaded, err := s.GetOne(c, id)
if err != nil {
return nil, err
}
results = append(results, *loaded)
}
return results, nil
}
type UniformityDetailItem struct {
Id int
Weight float64
Range string
}
type UniformityCalculation struct {
ChickQtyOfWeight float64
MeanWeight float64
MeanDown float64
MeanUp float64
UniformQty float64
OutsideQty float64
Uniformity float64
Cv float64
Details []UniformityDetailItem
}
type UniformityStandard struct {
MeanWeight *float64
Uniformity *float64
}
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
return computeUniformity(rows)
}
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) {
if s.DocumentSvc == nil {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
if err != nil {
return UniformityCalculation{}, nil, err
}
if len(documents) == 0 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
}
document := documents[0]
url := s.DocumentSvc.PublicURL(document)
if url == "" {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
}
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
if err != nil {
return UniformityCalculation{}, nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return UniformityCalculation{}, nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
}
rows, err := parseBodyWeightExcelReader(resp.Body)
if err != nil {
return UniformityCalculation{}, nil, err
}
calculation, err := computeUniformity(rows)
if err != nil {
return UniformityCalculation{}, nil, err
}
return calculation, &document, nil
}
func (s *uniformityService) createUniformityApproval(
ctx context.Context,
db *gorm.DB,
uniformityID uint,
step approvalutils.ApprovalStep,
action entity.ApprovalAction,
actorID uint,
notes *string,
) error {
if uniformityID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Uniformity tidak valid untuk approval")
}
if actorID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
}
var svc commonSvc.ApprovalService
if db != nil {
svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
} else if s.ApprovalSvc != nil {
svc = s.ApprovalSvc
} else {
svc = commonSvc.NewApprovalService(s.ApprovalRepo)
}
_, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowUniformity, uniformityID, step, &action, actorID, notes)
return err
}
func (s *uniformityService) attachLatestApprovals(ctx context.Context, items []entity.ProjectFlockKandangUniformity) error {
if len(items) == 0 || s.ApprovalSvc == nil {
return nil
}
ids := make([]uint, 0, len(items))
visited := make(map[uint]struct{}, len(items))
for _, item := range items {
if item.Id == 0 {
continue
}
if _, ok := visited[item.Id]; ok {
continue
}
visited[item.Id] = struct{}{}
ids = append(ids, item.Id)
}
if len(ids) == 0 {
return nil
}
latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowUniformity, ids, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load latest approvals for uniformities: %+v", err)
return nil
}
if len(latestMap) == 0 {
return nil
}
for i := range items {
if items[i].Id == 0 {
continue
}
if approval, ok := latestMap[items[i].Id]; ok {
items[i].LatestApproval = approval
}
}
return nil
}
func (s *uniformityService) attachLatestApproval(ctx context.Context, item *entity.ProjectFlockKandangUniformity) error {
if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
return nil
}
approvals, err := s.ApprovalSvc.ListByTarget(ctx, utils.ApprovalWorkflowUniformity, item.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load approvals for uniformity %d: %+v", item.Id, err)
return nil
}
if len(approvals) == 0 {
item.LatestApproval = nil
return nil
}
latest := approvals[len(approvals)-1]
item.LatestApproval = &latest
return nil
}
func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
return nil, nil
}
standard, err := s.resolveCategoryStandard(ctx, item.ProjectFlockKandang.ProjectFlock.Category, nil)
if err != nil || standard == nil {
return nil, err
}
detail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standard.Id, item.Week)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &UniformityStandard{
MeanWeight: cloneFloat64(detail.TargetMeanBw),
Uniformity: float64Ptr(detail.MinUniformity),
}, nil
}
func (s *uniformityService) resolveCategoryStandard(
ctx context.Context,
category string,
cache map[string]*entity.ProductionStandard,
) (*entity.ProductionStandard, error) {
category = strings.TrimSpace(category)
if category == "" {
return nil, nil
}
if cache != nil {
if cached, ok := cache[category]; ok {
return cached, nil
}
}
var standard entity.ProductionStandard
err := s.ProductionStandardRepo.DB().WithContext(ctx).
Where("project_category = ?", category).
Where("deleted_at IS NULL").
Order("created_at DESC").
First(&standard).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if cache != nil {
cache[category] = nil
}
return nil, nil
}
return nil, err
}
standardCopy := standard
if cache != nil {
cache[category] = &standardCopy
}
return &standardCopy, nil
}
func cloneFloat64(value *float64) *float64 {
if value == nil {
return nil
}
copy := *value
return &copy
}
func float64Ptr(value float64) *float64 {
copy := value
return &copy
}
func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) {
if uniformityID == 0 {
return
}
if s.ApprovalRepo != nil {
if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil {
s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID)
}
}
if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil {
s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID)
}
}
func uniqueUintSlice(values []uint) []uint {
if len(values) == 0 {
return nil
}
seen := make(map[uint]struct{}, len(values))
result := make([]uint, 0, len(values))
for _, v := range values {
if v == 0 {
continue
}
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
result = append(result, v)
}
return result
}
func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
weights := make([]float64, 0, len(rows))
details := make([]UniformityDetailItem, 0, len(rows))
hasRangeLabels := false
for idx, row := range rows {
if row.Weight <= 0 {
continue
}
id := row.No
if id <= 0 {
id = idx + 1
}
weights = append(weights, row.Weight)
rangeLabel := strings.TrimSpace(row.Range)
if rangeLabel != "" {
upper := strings.ToUpper(rangeLabel)
if upper == "HIGH" || upper == "LOW" {
hasRangeLabels = true
}
rangeLabel = upper
}
details = append(details, UniformityDetailItem{
Id: id,
Weight: row.Weight,
Range: rangeLabel,
})
}
total := float64(len(weights))
if total == 0 {
return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
}
var sum float64
for _, w := range weights {
sum += w
}
mean := sum / total
meanUpThreshold := roundToPrecision(mean*1.10, 3)
meanDownThreshold := roundToPrecision(mean*0.90, 3)
var uniformCount float64
for i := range details {
if hasRangeLabels {
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
details[i].Range = "Outside"
continue
}
details[i].Range = "Ideal"
uniformCount++
continue
}
w := details[i].Weight
if w > meanUpThreshold || w < meanDownThreshold {
details[i].Range = "Outside"
continue
}
details[i].Range = "Ideal"
uniformCount++
}
outsideCount := total - uniformCount
var cv float64
if mean > 0 && total > 1 {
stddevWeights := weights
stddevCount := float64(len(stddevWeights))
if stddevCount > 1 {
var stddevSum float64
for _, w := range stddevWeights {
stddevSum += w
}
stddevMean := stddevSum / stddevCount
var sumSquares float64
for _, w := range stddevWeights {
diff := w - stddevMean
sumSquares += diff * diff
}
stddev := math.Sqrt(sumSquares / (stddevCount - 1))
cv = (stddev / mean) * 100
}
}
uniformity := (uniformCount / total) * 100
return UniformityCalculation{
ChickQtyOfWeight: total,
MeanWeight: roundToPrecision(mean, 0),
MeanDown: roundToPrecision(mean*0.90, 0),
MeanUp: roundToPrecision(mean*1.10, 0),
UniformQty: uniformCount,
OutsideQty: outsideCount,
Uniformity: roundToPrecision(uniformity, 0),
Cv: roundToPrecision(cv, 1),
Details: details,
}, nil
}
func roundToPrecision(value float64, precision int) float64 {
if precision < 0 {
return value
}
scale := math.Pow10(precision)
scaled := value * scale
fraction := scaled - math.Floor(scaled)
if fraction >= 0.5 {
return math.Ceil(scaled) / scale
}
return math.Floor(scaled) / scale
}
@@ -0,0 +1,164 @@
package validation
import (
"mime/multipart"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
)
type Create struct {
Date string `form:"date" validate:"required"`
ProjectFlockKandangId uint `form:"project_flock_kandang_id" validate:"required,number,min=1"`
Week int `form:"week" validate:"required,min=1"`
}
type Update struct {
Date *string `json:"date,omitempty" form:"date" validate:"omitempty"`
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty" form:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
Week *int `json:"week,omitempty" form:"week" validate:"omitempty,min=1"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
Week int `query:"week" validate:"omitempty,min=1"`
}
type UploadExcelRequest struct {
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
func ParseIDParam(c *fiber.Ctx, name string) (uint, error) {
raw := strings.TrimSpace(c.Params(name))
if raw == "" {
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
id, err := strconv.Atoi(raw)
if err != nil || id <= 0 {
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
return uint(id), nil
}
func ParseQuery(c *fiber.Ctx) (*Query, error) {
query := &Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
Week: c.QueryInt("week", 0),
}
if query.Page < 1 || query.Limit < 1 {
return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
return query, nil
}
func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
date := strings.TrimSpace(c.FormValue("date"))
if date == "" {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "date is required")
}
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
if projectFlockKandangIDStr == "" {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
if err != nil || projectFlockKandangID <= 0 {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}
weekStr := strings.TrimSpace(c.FormValue("week"))
if weekStr == "" {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
}
week, err := strconv.Atoi(weekStr)
if err != nil || week <= 0 {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
}
file, err := c.FormFile("document")
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
}
return &Create{
Date: date,
ProjectFlockKandangId: uint(projectFlockKandangID),
Week: week,
}, file, nil
}
func ParseUpdate(c *fiber.Ctx) (*Update, *multipart.FileHeader, error) {
contentType := strings.ToLower(c.Get("Content-Type"))
if strings.Contains(contentType, "multipart/form-data") {
req := &Update{}
date := strings.TrimSpace(c.FormValue("date"))
if date != "" {
req.Date = &date
}
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
if projectFlockKandangIDStr != "" {
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
if err != nil || projectFlockKandangID <= 0 {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is invalid")
}
idCopy := uint(projectFlockKandangID)
req.ProjectFlockKandangId = &idCopy
}
weekStr := strings.TrimSpace(c.FormValue("week"))
if weekStr != "" {
week, err := strconv.Atoi(weekStr)
if err != nil || week <= 0 {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is invalid")
}
req.Week = &week
}
file, err := c.FormFile("document")
if err != nil {
file = nil
}
return req, file, nil
}
req := new(Update)
if err := c.BodyParser(req); err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
return req, nil, nil
}
func ParseUploadFiles(c *fiber.Ctx) ([]*multipart.FileHeader, error) {
file, err := c.FormFile("document")
if err != nil || file == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
}
return []*multipart.FileHeader{file}, nil
}
func ParseApprove(c *fiber.Ctx) (*Approve, error) {
req := new(Approve)
if err := c.BodyParser(req); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
return req, nil
}
@@ -1,6 +1,7 @@
package controller package controller
import ( import (
"encoding/json"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@@ -24,13 +25,13 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController {
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
CreatedFrom: strings.TrimSpace(c.Query("created_from")), CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")), CreatedTo: strings.TrimSpace(c.Query("created_to")),
SupplierID: uint(c.QueryInt("supplier_id", 0)), SupplierID: uint(c.QueryInt("supplier_id", 0)),
AreaID: uint(c.QueryInt("area_id", 0)), AreaID: uint(c.QueryInt("area_id", 0)),
LocationID: uint(c.QueryInt("location_id", 0)), LocationID: uint(c.QueryInt("location_id", 0)),
ProductCategoryID: uint(c.QueryInt("product_category_id", 0)), ProductCategoryID: uint(c.QueryInt("product_category_id", 0)),
} }
@@ -86,7 +87,6 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := ctrl.service.CreateOne(c, req) result, err := ctrl.service.CreateOne(c, req)
if err != nil { if err != nil {
return err return err
@@ -161,10 +161,26 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
} }
req := new(validation.ReceivePurchaseRequest) req := new(validation.ReceivePurchaseRequest)
if err := c.BodyParser(req); err != nil { form, err := c.MultipartForm()
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Action = c.FormValue("action")
if notes := strings.TrimSpace(c.FormValue("notes")); notes != "" {
req.Notes = &notes
} }
itemsJSON := c.FormValue("items")
if strings.TrimSpace(itemsJSON) != "" {
if err := json.Unmarshal([]byte(itemsJSON), &req.Items); err != nil {
var singleItem validation.ReceivePurchaseItemRequest
if err := json.Unmarshal([]byte(itemsJSON), &singleItem); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid items JSON")
}
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
}
}
req.TravelDocuments = form.File["documents"]
result, err := ctrl.service.ReceiveProducts(c, uint(id), req) result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
if err != nil { if err != nil {
return err return err
+1
View File
@@ -98,6 +98,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalService, approvalService,
expenseBridge, expenseBridge,
fifoService, fifoService,
documentSvc,
) )
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
@@ -310,9 +310,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
return err return err
} }
if cnt == 1 { 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) newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
if err != nil { if err != nil {
return err return err
@@ -332,7 +329,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
"price": pricePerItem, "price": pricePerItem,
"notes": note, "notes": note,
"nonstock_id": newNonstockID, "nonstock_id": newNonstockID,
"kandang_id": uint64(*item.Warehouse.KandangId), }
if item.Warehouse != nil && item.Warehouse.KandangId != nil && *item.Warehouse.KandangId != 0 {
updateBody["kandang_id"] = uint64(*item.Warehouse.KandangId)
} }
if err := b.db.WithContext(ctx). if err := b.db.WithContext(ctx).
Model(&entity.ExpenseNonstock{}). Model(&entity.ExpenseNonstock{}).
@@ -395,9 +394,13 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
} }
if kandangID != nil { if kandangID != nil {
updateBody["kandang_id"] = uint64(*kandangID) updateBody["kandang_id"] = uint64(*kandangID)
} else {
updateBody["kandang_id"] = nil
} }
if projectFK != nil { if projectFK != nil {
updateBody["project_flock_kandang_id"] = uint64(*projectFK) updateBody["project_flock_kandang_id"] = uint64(*projectFK)
} else {
updateBody["project_flock_kandang_id"] = nil
} }
if err := b.db.WithContext(ctx). if err := b.db.WithContext(ctx).
@@ -550,18 +553,27 @@ func (b *expenseBridge) createExpenseViaService(
} }
kandangID := items[0].kandangID kandangID := items[0].kandangID
if kandangID == nil || *kandangID == 0 { var locationID uint64
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs") var expenseKandangID *uint64
} if kandangID != nil && *kandangID != 0 {
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB { return db.Select("id, location_id")
return db.Select("id, location_id") })
}) if err != nil {
if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) }
} if kandang == nil {
if kandang == nil { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) }
locationID = uint64(kandang.LocationId)
id := uint64(*kandangID)
expenseKandangID = &id
} else {
warehouse := items[0].item.Warehouse
if warehouse == nil || warehouse.LocationId == nil || *warehouse.LocationId == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse location is required for expense")
}
locationID = uint64(*warehouse.LocationId)
} }
costItems := make([]expenseValidation.CostItem, 0, len(items)) costItems := make([]expenseValidation.CostItem, 0, len(items))
@@ -584,9 +596,9 @@ func (b *expenseBridge) createExpenseViaService(
TransactionDate: utils.FormatDate(expenseDate), TransactionDate: utils.FormatDate(expenseDate),
Category: "BOP", Category: "BOP",
SupplierID: uint64(supplierID), SupplierID: uint64(supplierID),
LocationID: uint64(kandang.LocationId), LocationID: locationID,
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{ ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
KandangID: func() *uint64 { id := uint64(*kandangID); return &id }(), KandangID: expenseKandangID,
CostItems: costItems, CostItems: costItems,
}}, }},
} }
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"mime/multipart"
"strings" "strings"
"time" "time"
@@ -57,6 +58,7 @@ type purchaseService struct {
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
ExpenseBridge PurchaseExpenseBridge ExpenseBridge PurchaseExpenseBridge
FifoSvc commonSvc.FifoService FifoSvc commonSvc.FifoService
DocumentSvc commonSvc.DocumentService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
@@ -76,6 +78,7 @@ func NewPurchaseService(
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
expenseBridge PurchaseExpenseBridge, expenseBridge PurchaseExpenseBridge,
fifoSvc commonSvc.FifoService, fifoSvc commonSvc.FifoService,
documentSvc commonSvc.DocumentService,
) PurchaseService { ) PurchaseService {
return &purchaseService{ return &purchaseService{
Log: utils.Log, Log: utils.Log,
@@ -89,6 +92,7 @@ func NewPurchaseService(
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
ExpenseBridge: expenseBridge, ExpenseBridge: expenseBridge,
FifoSvc: fifoSvc, FifoSvc: fifoSvc,
DocumentSvc: documentSvc,
approvalWorkflow: utils.ApprovalWorkflowPurchase, approvalWorkflow: utils.ApprovalWorkflowPurchase,
} }
} }
@@ -246,22 +250,25 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err) s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
return nil, nil, utils.Internal("Failed to get warehouse") return nil, nil, utils.Internal("Failed to get warehouse")
} }
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name))
}
var pfkID *uint var pfkID *uint
if s.ProjectFlockKandangRepo != nil { isKandang := strings.EqualFold(strings.TrimSpace(warehouse.Type), "KANDANG")
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil { if isKandang {
if pfk.ClosedAt != nil { if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
return nil, nil, utils.BadRequest("Project sudah closing") return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name))
}
if s.ProjectFlockKandangRepo != nil {
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
if pfk.ClosedAt != nil {
return nil, nil, utils.BadRequest("Project sudah closing")
}
idCopy := uint(pfk.Id)
pfkID = &idCopy
} else if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name))
} else if err != nil {
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
return nil, nil, utils.Internal("Failed to validate project flock")
} }
idCopy := uint(pfk.Id)
pfkID = &idCopy
} else if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name))
} else if err != nil {
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
return nil, nil, utils.Internal("Failed to validate project flock")
} }
} }
@@ -612,9 +619,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
ctx := c.Context() ctx := c.Context()
action, err := parseApprovalActionInput(req.Action) action, err := parseApprovalActionInput(req.Action)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -661,6 +666,30 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return updated, nil return updated, nil
} }
if action == entity.ApprovalActionApproved && len(req.TravelDocuments) > 0 {
if len(req.TravelDocuments) > len(req.Items) {
return nil, utils.BadRequest("Travel documents exceed total receiving items")
}
for idx, file := range req.TravelDocuments {
if file == nil {
continue
}
if idx >= len(req.Items) {
break
}
itemID := req.Items[idx].PurchaseItemID
if itemID == 0 {
return nil, utils.BadRequest("Purchase item id is required for travel document upload")
}
uploadedURL, err := s.uploadTravelDocument(ctx, actorID, itemID, file)
if err != nil {
s.Log.Errorf("Failed to upload travel document for item %d: %+v", itemID, err)
return nil, utils.Internal("Failed to upload travel document")
}
req.Items[idx].TravelDocumentPath = &uploadedURL
}
}
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items)) itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
for i := range purchase.Items { for i := range purchase.Items {
itemMap[purchase.Items[i].Id] = &purchase.Items[i] itemMap[purchase.Items[i].Id] = &purchase.Items[i]
@@ -804,32 +833,20 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
for _, prep := range prepared { for _, prep := range prepared {
item := prep.item item := prep.item
var oldPWID *uint
if item.ProductWarehouseId != nil {
idCopy := uint(*item.ProductWarehouseId)
oldPWID = &idCopy
}
var newPWID *uint var newPWID *uint
clearPW := false
// Always ensure PW when qty > 0 so stockable has target. // Always ensure PW after receiving so linkage stays stable.
if prep.receivedQty > 0 { pwID, err := pwRepoTx.EnsureProductWarehouse(
pwID, err := pwRepoTx.EnsureProductWarehouse( c.Context(),
c.Context(), uint(item.ProductId),
uint(item.ProductId), prep.warehouseID,
prep.warehouseID, item.ProjectFlockKandangId,
item.ProjectFlockKandangId, purchase.CreatedBy,
purchase.CreatedBy, )
) if err != nil {
if err != nil { return err
return err
}
newPWID = &pwID
} else if oldPWID != nil {
newPWID = oldPWID
clearPW = true
} }
newPWID = &pwID
deltaQty := prep.receivedQty - item.TotalQty deltaQty := prep.receivedQty - item.TotalQty
switch { switch {
@@ -854,7 +871,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
VehicleNumber: prep.payload.VehicleNumber, VehicleNumber: prep.payload.VehicleNumber,
ReceivedQty: &qtyCopy, ReceivedQty: &qtyCopy,
ProductWarehouseID: newPWID, ProductWarehouseID: newPWID,
ClearProductWarehouse: clearPW, ClearProductWarehouse: false,
} }
if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID { if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID {
@@ -969,6 +986,38 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return updated, nil return updated, nil
} }
func (s *purchaseService) uploadTravelDocument(
ctx context.Context,
actorID uint,
itemID uint,
file *multipart.FileHeader,
) (string, error) {
if file == nil {
return "", errors.New("travel document file is required")
}
if s.DocumentSvc == nil {
return "", errors.New("document service not available")
}
documentFiles := []commonSvc.DocumentFile{{
File: file,
Type: string(utils.DocumentTypePurchaseTravel),
}}
results, err := s.DocumentSvc.UploadDocuments(ctx, commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypePurchaseItem),
DocumentableID: uint64(itemID),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return "", err
}
if len(results) == 0 {
return "", errors.New("upload result is empty")
}
return results[0].URL, nil
}
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) { func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
@@ -1048,6 +1097,10 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
return nil, utils.Internal("Failed to delete purchase items") return nil, utils.Internal("Failed to delete purchase items")
} }
if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil {
return nil, utils.Internal("Failed to delete purchase documents")
}
if len(itemsToDelete) > 0 { if len(itemsToDelete) > 0 {
if err := s.notifyExpenseItemsDeleted(ctx, purchase.Id, itemsToDelete); err != nil { 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) s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", purchase.Id, err)
@@ -1107,6 +1160,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
return utils.Internal("Failed to delete purchase") return utils.Internal("Failed to delete purchase")
} }
if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil {
return utils.Internal("Failed to delete purchase documents")
}
if len(itemsToDelete) > 0 { if len(itemsToDelete) > 0 {
if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil { if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil {
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err) s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err)
@@ -1190,6 +1247,21 @@ func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchas
} }
func (s *purchaseService) deletePurchaseItemDocuments(ctx context.Context, items []entity.PurchaseItem) error {
if s.DocumentSvc == nil || len(items) == 0 {
return nil
}
for _, item := range items {
if item.Id == 0 {
continue
}
if err := s.DocumentSvc.DeleteByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id), true); err != nil {
return err
}
}
return nil
}
func (s *purchaseService) buildStaffAdjustmentPayload( func (s *purchaseService) buildStaffAdjustmentPayload(
ctx context.Context, ctx context.Context,
purchase *entity.Purchase, purchase *entity.Purchase,
@@ -1462,5 +1534,5 @@ func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
return utils.Internal("DB not available for project flock validation") return utils.Internal("DB not available for project flock validation")
} }
return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs) return commonSvc.EnsureProjectFlockNotClosedByProjectFlockKandangID(ctx, db, pfkIDs)
} }
@@ -1,5 +1,7 @@
package validation package validation
import "mime/multipart"
type PurchaseItemPayload struct { type PurchaseItemPayload struct {
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"` WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
ProductID uint `json:"product_id" validate:"required,gt=0"` ProductID uint `json:"product_id" validate:"required,gt=0"`
@@ -26,7 +28,7 @@ type StaffPurchaseApprovalItem struct {
type ApproveStaffPurchaseRequest struct { type ApproveStaffPurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Items []StaffPurchaseApprovalItem `json:"items,omitempty" validate:"omitempty,min=1,dive"` Items []StaffPurchaseApprovalItem `json:"items" validate:"omitempty,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
@@ -36,21 +38,22 @@ type ApproveManagerPurchaseRequest struct {
} }
type ReceivePurchaseItemRequest struct { type ReceivePurchaseItemRequest struct {
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"` PurchaseItemID uint `form:"purchase_item_id" json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"` WarehouseID *uint `form:"warehouse_id" json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"` ReceivedDate string `form:"received_date" json:"received_date" validate:"required,datetime=2006-01-02"`
ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"` ExpeditionVendorID *uint `form:"expedition_vendor_id" json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"` TransportPerItem *float64 `form:"transport_per_item" json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"` TravelNumber *string `form:"travel_number" json:"travel_number" validate:"omitempty,max=100"`
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"` TravelDocumentPath *string `form:"travel_document_path" json:"travel_document_path" validate:"omitempty,max=1024"`
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"` VehicleNumber *string `form:"vehicle_number" json:"vehicle_number" validate:"omitempty,max=100"`
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"` ReceivedQty *float64 `form:"received_qty" json:"received_qty" validate:"omitempty,gte=0"`
} }
type ReceivePurchaseRequest struct { type ReceivePurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"`
Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"` Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"`
Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type DeletePurchaseItemsRequest struct { type DeletePurchaseItemsRequest struct {
+1 -1
View File
@@ -18,6 +18,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang) route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll),ctrl.GetHppPerKandang)
} }
+21 -4
View File
@@ -289,6 +289,21 @@ var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
RecordingStepDisetujui: "Disetujui", RecordingStepDisetujui: "Disetujui",
} }
// -------------------------------------------------------------------
// Uniformity Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowUniformity approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("UNIFORMITIES")
UniformityStepPengajuan approvalutils.ApprovalStep = 1
UniformityStepDisetujui approvalutils.ApprovalStep = 2
)
var UniformityApprovalSteps = map[approvalutils.ApprovalStep]string{
UniformityStepPengajuan: "Pengajuan",
UniformityStepDisetujui: "Disetujui",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Purchase Approval // Purchase Approval
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -408,13 +423,15 @@ type DocumentType string
type DocumentableType string type DocumentableType string
const ( const (
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT" DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT" DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT" DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -5,31 +5,6 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
) )
func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.RecordingBW {
if len(items) == 0 {
return nil
}
result := make([]entity.RecordingBW, 0, len(items))
for _, item := range items {
var totalWeight float64
if item.TotalWeight != nil {
totalWeight = *item.TotalWeight
}
if totalWeight <= 0 {
totalWeight = item.AvgWeight * item.Qty
}
result = append(result, entity.RecordingBW{
RecordingId: recordingID,
AvgWeight: item.AvgWeight,
Qty: item.Qty,
TotalWeight: totalWeight,
})
}
return result
}
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock { func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
if len(items) == 0 { if len(items) == 0 {
return nil return nil
@@ -86,20 +61,3 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.
} }
return result return result
} }
func ToGrams(weight float64) float64 {
if weight <= 0 {
return 0
}
if weight < 10 {
return weight * 1000
}
return weight
}
func GramsToKg(grams float64) float64 {
if grams <= 0 {
return 0
}
return grams / 1000
}