Compare commits

..

24 Commits

Author SHA1 Message Date
Hafizh A. Y. 2d098cb6b1 Merge branch 'feat/BE/US-281-uniformity' into 'feat/BE/Sprint-8'
feat(BE-281): uniformity and create project flock triger... unfinished s3 read

See merge request mbugroup/lti-api!121
2025-12-31 02:20:01 +00:00
ragilap 709e304f7f feat(BE-281): adjustment bug erorr 500 if 404 record projectflock 2025-12-31 07:39:20 +07:00
ragilap d994cfdce7 productstock 2025-12-31 07:08:03 +07:00
Hafizh A. Y. d3bb00a06a Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): nonstock response supplier null to empty array

See merge request mbugroup/lti-api!120
2025-12-30 23:57:54 +00:00
Hafizh A. Y 5302713811 fix(be): nonstock response supplier null to empty array 2025-12-31 06:52:38 +07:00
Hafizh A. Y. f698ca070c Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): remove omitempty in dto and validation nonstock

See merge request mbugroup/lti-api!118
2025-12-30 23:44:27 +00:00
Hafizh A. Y 6c42119f4d fix(be): remove omitempty in dto and validation nonstock 2025-12-31 06:43:34 +07:00
Hafizh A. Y. 299c8c7177 Merge branch 'fix/setup-seeder' into 'development'
fix: setup seeder for development

See merge request mbugroup/lti-api!117
2025-12-30 16:54:23 +00:00
Hafizh A. Y 78359db880 fix: setup seeder for development 2025-12-30 23:52:37 +07:00
Hafizh A. Y. 10799cc1ed Merge branch 'feat/BE/Sprint-8' into 'development'
[FEAT/BE] Partial Merge #3

See merge request mbugroup/lti-api!116
2025-12-30 12:06:19 +00:00
Hafizh A. Y. c9c581ef30 Merge branch 'sprint-8/gio' into 'feat/BE/Sprint-8'
fix api get all closing; fix get closing sapronak; fix get all maste data product

See merge request mbugroup/lti-api!115
2025-12-30 09:36:48 +00:00
Hafizh A. Y. 6ee795cf2a Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
feat(BE US#386): add standard_fcr column to production_standard_details and update existing API

See merge request mbugroup/lti-api!114
2025-12-30 09:36:00 +00:00
aguhh18 471fd1dbbf feat(BE): enhance product warehouse handling and automatic calculations for delivery and sales orders 2025-12-30 16:30:44 +07:00
ragilap 4e5caa8cba feat(BE-281): add rbac for uniformity 2025-12-30 15:23:34 +07:00
ragilap 0c776e8332 feat(BE-281): uncoment auth 2025-12-30 12:08:49 +07:00
ragilap 90125ffe1a feat(BE-281):add dto standart mean bw and uniformity 2025-12-30 12:07:28 +07:00
aguhh18 c36719cc1a Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-30 10:42:04 +07:00
aguhh18 e4acd9a21e feat(BE): add standard_fcr column to production_standard_details and update related services and validations 2025-12-30 10:27:12 +07:00
ragilap 9a094b8bfe feat(BE-281):fix document payload 2025-12-30 09:56:48 +07:00
Hafizh A. Y. 49af2d6448 Merge branch 'feat/BE/Sprint-8' into 'development'
Feat[BE]: Partial Merge

See merge request mbugroup/lti-api!112
2025-12-29 14:39:58 +00:00
ragilap 6523290aaf feat(BE-281): change template excel 2025-12-29 19:44:10 +07:00
ragilap a2066979c1 feat(BE-281): adjustment uniformity for make unique for week,projectflockandang, and date 2025-12-29 19:04:10 +07:00
ragilap 8dfb224614 feat(BE-281): changes std deviasi first 100 data to all 2025-12-29 10:13:29 +07:00
ragilap 644896edfa feat(BE-281): unfinished uniformity and create project flock triger productwarehouse and add new filtering lookup 2025-12-29 00:21:26 +07:00
42 changed files with 3639 additions and 806 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,3 @@
-- Remove standard_fcr column from production_standard_details table
ALTER TABLE production_standard_details
DROP COLUMN IF EXISTS standard_fcr;
@@ -0,0 +1,3 @@
-- Add standard_fcr column to production_standard_details table
ALTER TABLE production_standard_details
ADD COLUMN standard_fcr NUMERIC(15, 3);
File diff suppressed because it is too large Load Diff
+139 -722
View File
@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -25,66 +24,20 @@ func Run(db *gorm.DB) error {
return err return err
} }
areas, err := seedAreas(tx, adminID)
if err != nil {
return err
}
locations, err := seedLocations(tx, adminID, areas)
if err != nil {
return err
}
productCategories, err := seedProductCategories(tx, adminID) productCategories, err := seedProductCategories(tx, adminID)
if err != nil { if err != nil {
return err return err
} }
if _, err := seedFlocks(tx, adminID); err != nil {
return err
}
if _, err := seedFcr(tx, adminID); err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID) suppliers, err := seedSuppliers(tx, adminID)
if err != nil { if err != nil {
return err return err
} }
if err := seedCustomers(tx, adminID, users); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil { if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err return err
} }
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
return err
}
if err := seedBanks(tx, adminID); err != nil {
return err
}
if err := seedProductWarehouse(tx, adminID); err != nil {
return err
}
if err := seedTransferStock(tx); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -141,224 +94,6 @@ func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Priangan", "Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var area entity.Area
err := tx.Where("name = ?", name).First(&area).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
area = entity.Area{Name: name, CreatedBy: createdBy}
if err := tx.Create(&area).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = area.Id
}
return result, nil
}
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Address string
Area string
}{
{"Singaparna", "Tasik", "Priangan"},
{"Cikaum", "Cikaum", "Banten"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
var loc entity.Location
err := tx.Where("name = ?", seed.Name).First(&loc).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
loc = entity.Location{
Name: seed.Name,
Address: seed.Address,
AreaId: areaID,
CreatedBy: createdBy,
}
if err := tx.Create(&loc).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = loc.Id
}
return result, nil
}
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Flock Priangan", "Flock Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var flock entity.Flock
err := tx.Where("name = ?", name).First(&flock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
flock = entity.Flock{
Name: name,
CreatedBy: createdBy,
}
if err := tx.Create(&flock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
"created_by": createdBy,
}).Error; err != nil {
return nil, err
}
}
result[name] = flock.Id
}
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Status utils.KandangStatus
Capacity float64
Location string
PicKey string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
locID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
picID, ok := users[seed.PicKey]
if !ok {
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
Status: string(seed.Status),
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
updates := map[string]any{
"location_id": locID,
"pic_id": picID,
"status": string(seed.Status),
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
return result, nil
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
Type string
Area string
Location *string
Kandang *string
}{
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
}
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return fmt.Errorf("area %s not seeded", seed.Area)
}
var warehouse entity.Warehouse
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
warehouse = entity.Warehouse{
Name: seed.Name,
Type: seed.Type,
AreaId: areaID,
CreatedBy: createdBy,
}
} else if err != nil {
return err
}
if seed.Location != nil {
locID, ok := locations[*seed.Location]
if !ok {
return fmt.Errorf("location %s not seeded", *seed.Location)
}
warehouse.LocationId = uintPtr(locID)
}
if seed.Kandang != nil {
kandangID, ok := kandangs[*seed.Kandang]
if !ok {
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
}
warehouse.KandangId = uintPtr(kandangID)
}
if warehouse.Id == 0 {
if err := tx.Create(&warehouse).Error; err != nil {
return err
}
} else {
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
"type": warehouse.Type,
"area_id": warehouse.AreaId,
"location_id": warehouse.LocationId,
"kandang_id": warehouse.KandangId,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) { func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -440,113 +175,6 @@ func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
seeds := []struct {
Name string
PicKey string
Address string
Phone string
Email string
}{
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
}
for idx, seed := range seeds {
picID, ok := users[seed.PicKey]
if !ok {
return fmt.Errorf("user %s not seeded", seed.PicKey)
}
var customer entity.Customer
err := tx.Where("name = ?", seed.Name).First(&customer).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
customer = entity.Customer{
Name: seed.Name,
PicId: picID,
Type: string(utils.CustomerSupplierTypeBisnis),
Address: seed.Address,
Phone: seed.Phone,
Email: seed.Email,
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
CreatedBy: createdBy,
}
if err := tx.Create(&customer).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Standards []struct {
Weight float64
FcrNumber float64
Mortality float64
}
}{
{
Name: "FCR Layer",
Standards: []struct {
Weight float64
FcrNumber float64
Mortality float64
}{
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
},
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = fcr.Id
for _, std := range seed.Standards {
var standard entity.FcrStandard
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
standard = entity.FcrStandard{
FcrID: fcr.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return nil, err
}
}
}
}
return result, nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error { func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -560,92 +188,88 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Expiry *int Expiry *int
Suppliers []string Suppliers []string
Flags []utils.FlagType Flags []utils.FlagType
IsVisible bool
}{ }{
{ {
Name: "DOC Broiler", Name: "ISA Brown",
Brand: "MBU Broiler", Brand: "ISA Brown",
Sku: "BRO0001", Sku: "ISA0001",
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 7500, Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC}, Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer},
IsVisible: true,
}, },
{ {
Name: "Ayam Pullet", Name: "Ayam Afkir",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Flags: []utils.FlagType{utils.FlagAyamAfkir},
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Flags: []utils.FlagType{utils.FlagAyamMati},
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Flags: []utils.FlagType{utils.FlagAyamCulling},
},
{
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurUtuh},
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPecah},
},
{
Name: "281 SPECIAL STARTER",
Brand: "281 STARTER",
Sku: "281",
Uom: "Kilogram",
Category: "Bahan Baku",
Price: 7850,
Expiry: intPtr(60),
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
},
{
Name: "Ayam Layer",
Brand: "-", Brand: "-",
Sku: "LYR0001", Sku: "1",
Uom: "Ekor", Uom: "Ekor",
Category: "Pullet", Category: "Day Old Chick",
Price: 20000, Price: 1,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagAyamAfkir},
Flags: []utils.FlagType{utils.FlagLayer}, IsVisible: false,
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Flags: []utils.FlagType{utils.FlagAyamMati},
IsVisible: false,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Flags: []utils.FlagType{utils.FlagAyamCulling},
IsVisible: false,
},
{
Name: "Telur Utuh",
Brand: "-",
Sku: "4",
Uom: "Gram",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurUtuh},
IsVisible: false,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Gram",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPecah},
IsVisible: false,
},
{
Name: "Telur Putih",
Brand: "-",
Sku: "6",
Uom: "Gram",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPutih},
IsVisible: false,
},
{
Name: "Telur Retak",
Brand: "-",
Sku: "7",
Uom: "Gram",
Category: "Telur",
Price: 1,
Flags: []utils.FlagType{utils.FlagTelurRetak},
IsVisible: false,
}, },
} }
@@ -724,78 +348,78 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
return nil return nil
} }
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { // func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
seeds := []struct { // seeds := []struct {
Name string // Name string
Uom string // Uom string
Suppliers []string // Suppliers []string
Flags []utils.FlagType // Flags []utils.FlagType
}{ // }{
{ // {
Name: "Expedisi DOC", // Name: "LAJ",
Uom: "Ekor", // Uom: "Unit",
Suppliers: []string{"Ekspedisi"}, // Suppliers: []string{"Ekspedisi"},
Flags: []utils.FlagType{utils.FlagEkspedisi}, // Flags: []utils.FlagType{utils.FlagEkspedisi},
}, // },
{ // {
Name: "Solar", // Name: "Solar",
Uom: "Liter", // Uom: "Liter",
Suppliers: []string{"BOP Vendor"}, // Suppliers: []string{"BOP Vendor"},
Flags: []utils.FlagType{}, // Flags: []utils.FlagType{},
}, // },
} // }
for _, seed := range seeds { // for _, seed := range seeds {
uomID, ok := uoms[seed.Uom] // uomID, ok := uoms[seed.Uom]
if !ok { // if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom) // return fmt.Errorf("uom %s not seeded", seed.Uom)
} // }
var nonstock entity.Nonstock // var nonstock entity.Nonstock
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error // err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
nonstock = entity.Nonstock{ // nonstock = entity.Nonstock{
Name: seed.Name, // Name: seed.Name,
UomId: uomID, // UomId: uomID,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&nonstock).Error; err != nil { // if err := tx.Create(&nonstock).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} else { // } else {
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ // if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
"uom_id": uomID, // "uom_id": uomID,
}).Error; err != nil { // }).Error; err != nil {
return err // return err
} // }
} // }
for _, supplierName := range seed.Suppliers { // for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName] // supplierID, ok := suppliers[supplierName]
if !ok { // if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName) // return fmt.Errorf("supplier %s not seeded", supplierName)
} // }
var existing entity.NonstockSupplier // var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error // err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} // link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil { // if err := tx.Create(&link).Error; err != nil {
return err // return err
} // }
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err // return err
} // }
} // }
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { // if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
return err // return err
} // }
} // }
return nil // return nil
} // }
// nanti saya isi // nanti saya isi
@@ -823,213 +447,6 @@ func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.
return nil return nil
} }
func seedBanks(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Alias string
Owner *string
AccountNumber string
}{
{
Name: "Bank Central Asia",
Alias: "BCA",
AccountNumber: "1234567890",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Rakyat Indonesia",
Alias: "BRI",
AccountNumber: "9876543210",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Mandiri",
Alias: "MAND",
AccountNumber: "1122334455",
Owner: ptr("PT MBU Group"),
},
}
for _, seed := range seeds {
var bank entity.Bank
err := tx.Where("name = ?", seed.Name).First(&bank).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
bank = entity.Bank{
Name: seed.Name,
Alias: seed.Alias,
Owner: seed.Owner,
AccountNumber: seed.AccountNumber,
CreatedBy: createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&bank).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// update data jika sudah ada
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
"alias": seed.Alias,
"owner": seed.Owner,
"account_number": seed.AccountNumber,
"updated_at": time.Now(),
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProductName string
WarehouseName string
Quantity float64
}{
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
}
for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouse.Id,
Quantity: seed.Quantity,
// CreatedBy: createdBy,
}
if err := tx.Create(&productWarehouse).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&productWarehouse).Updates(map[string]any{
"quantity": seed.Quantity,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedTransferStock(tx *gorm.DB) error {
transfer := entity.StockTransfer{
FromWarehouseId: 1,
ToWarehouseId: 2,
Reason: "Seed transfer stock",
TransferDate: time.Now(),
MovementNumber: "SEED-TRF-00001",
CreatedBy: 1,
}
if err := tx.Create(&transfer).Error; err != nil {
return err
}
details := []entity.StockTransferDetail{
{
StockTransferId: transfer.Id,
ProductId: 1,
// Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
// Quantity: 5,
},
}
for i := range details {
if err := tx.Create(&details[i]).Error; err != nil {
return err
}
}
deliveries := []entity.StockTransferDelivery{
{
StockTransferId: transfer.Id,
SupplierId: 1,
VehiclePlate: "B 1234 XYZ",
DriverName: "Driver Seed",
DocumentPath: "seed.pdf",
ShippingCostItem: 1000,
ShippingCostTotal: 2000,
},
}
for i := range deliveries {
if err := tx.Create(&deliveries[i]).Error; err != nil {
return err
}
}
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
deliveryItems := []entity.StockTransferDeliveryItem{
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[1],
Quantity: 50,
},
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[2],
Quantity: 30,
},
}
for i := range deliveryItems {
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
func strPtr(s string) *string { func strPtr(s string) *string {
return &s return &s
} }
func intPtr(v int) *int {
return &v
}
func uintPtr(v uint) *uint {
return &v
}
@@ -12,6 +12,7 @@ type ProductionStandardDetail struct {
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"` TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"` TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
TargetEggMass *float64 `gorm:"type:numeric(15,3)"` TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
StandardFCR *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"` CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"` UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
@@ -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"
}
+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"`
}
+7 -6
View File
@@ -218,12 +218,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 (
@@ -117,39 +117,37 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var createdLogId uint var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) var projectFlockKandangID *uint
if err != nil { pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(req.WarehouseID))
s.Log.Errorf("Failed to check product warehouse existence: %+v", err) if err == nil && pfk != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse") idCopy := uint(pfk.Id)
projectFlockKandangID = &idCopy
} }
if !isProductWarehouseExist {
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID)) pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
if err != nil { ctx,
return nil, err uint(req.ProductID),
uint(req.WarehouseID),
projectFlockKandangID,
)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to find product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
newPW := &entity.ProductWarehouse{ newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID), ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID), WarehouseId: uint(req.WarehouseID),
Quantity: 0, Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID, ProjectFlockKandangId: projectFlockKandangID,
// CreatedBy: 1, // TODO: should Get from auth middleware
} }
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err) s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
} }
s.Log.Infof("Product warehouse created: %+v", newPW.Id) pw = newPW
}
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
ctx,
uint(req.ProductID),
uint(req.WarehouseID),
)
if err != nil {
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
} }
if err := common.EnsureProjectFlockNotClosedForProductWarehouses( if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
@@ -18,6 +18,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
@@ -28,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 {
@@ -107,6 +110,20 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
return &productWarehouse, nil return &productWarehouse, nil
} }
func (r *ProductWarehouseRepositoryImpl) FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT DISTINCT FROM ?", productID, warehouseID, projectFlockKandangID).
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse var productWarehouses []entity.ProductWarehouse
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
@@ -257,6 +274,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).
@@ -106,23 +106,17 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
} }
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) { func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db) return s.withRelations(db)
}) })
if err != nil { if err != nil {
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
} }
if transferPtr != nil {
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
}
return transferPtr, nil return transferPtr, nil
} }
@@ -336,7 +330,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Files: documentFiles, Files: documentFiles,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1)) s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
idx+1, deliveries[idx].Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err))
} }
} }
} }
@@ -396,7 +392,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
if err != nil { if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
} }
@@ -247,11 +247,15 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate itemDeliveryDate = &parsedDate
} }
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -357,11 +361,15 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -75,7 +75,6 @@ func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, er
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get marketing by id: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
} }
@@ -293,13 +292,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, rp := range req.MarketingProducts { for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok { if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
updateBody := map[string]any{ updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId, "product_warehouse_id": rp.ProductWarehouseId,
"qty": rp.Qty, "qty": rp.Qty,
"unit_price": rp.UnitPrice, "unit_price": rp.UnitPrice,
"avg_weight": rp.AvgWeight, "avg_weight": rp.AvgWeight,
"total_weight": rp.TotalWeight, "total_weight": totalWeight,
"total_price": rp.TotalPrice, "total_price": totalPrice,
} }
if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
@@ -589,30 +592,34 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{ marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId, MarketingId: marketingId,
ProductWarehouseId: rp.ProductWarehouseId, ProductWarehouseId: rp.ProductWarehouseId,
Qty: rp.Qty, Qty: rp.Qty,
UnitPrice: rp.UnitPrice, UnitPrice: rp.UnitPrice,
AvgWeight: rp.AvgWeight, AvgWeight: rp.AvgWeight,
TotalWeight: rp.TotalWeight, TotalWeight: totalWeight,
TotalPrice: rp.TotalPrice, TotalPrice: totalPrice,
} }
if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
return err return err
} }
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
MarketingProductId: marketingProduct.Id, MarketingProductId: marketingProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
UnitPrice: 0, UnitPrice: 0,
TotalWeight: 0, TotalWeight: 0,
AvgWeight: 0, AvgWeight: 0,
TotalPrice: 0, TotalPrice: 0,
DeliveryDate: nil, DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber, VehicleNumber: rp.VehicleNumber,
UsageQty: 0, UsageQty: 0,
PendingQty: 0, PendingQty: 0,
} }
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
return err return err
@@ -5,8 +5,6 @@ type DeliveryProduct struct {
Qty float64 `json:"qty" validate:"omitempty,gte=0"` Qty float64 `json:"qty" validate:"omitempty,gte=0"`
UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"` UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"`
AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"` AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"`
TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"`
TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"`
DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"`
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
} }
@@ -12,10 +12,8 @@ type CreateMarketingProduct struct {
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"`
UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"`
TotalWeight float64 `json:"total_weight" validate:"required,gt=0"`
Qty float64 `json:"qty" validate:"required,gt=0"` Qty float64 `json:"qty" validate:"required,gt=0"`
AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
} }
type Update struct { type Update struct {
@@ -1,11 +1,12 @@
package dto package dto
import ( import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"time"
) )
// === DTO Structs === // === DTO Structs ===
@@ -22,7 +23,7 @@ type NonstockListDTO struct {
Name string `json:"name"` Name string `json:"name"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom"` Uom *uomDTO.UomRelationDTO `json:"uom"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"` Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -100,7 +101,7 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO { func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 { if len(relations) == 0 {
return nil return make([]supplierDTO.SupplierRelationDTO, 0)
} }
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
@@ -112,7 +113,7 @@ func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.S
} }
if len(result) == 0 { if len(result) == 0 {
return nil return make([]supplierDTO.SupplierRelationDTO, 0)
} }
return result return result
@@ -3,8 +3,8 @@ package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"` UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` Flags []string `json:"flags" validate:"dive,max=50"`
} }
type Update struct { type Update struct {
@@ -33,6 +33,7 @@ type EggProductionStandardDetailDTO struct {
TargetHenHouseProduction *float64 `json:"target_hen_house_production"` TargetHenHouseProduction *float64 `json:"target_hen_house_production"`
TargetEggWeight *float64 `json:"target_egg_weight"` TargetEggWeight *float64 `json:"target_egg_weight"`
TargetEggMass *float64 `json:"target_egg_mass"` TargetEggMass *float64 `json:"target_egg_mass"`
StandardFCR *float64 `json:"standard_fcr"`
} }
type WeeklyProductionStandardDTO struct { type WeeklyProductionStandardDTO struct {
@@ -95,6 +96,7 @@ func ToWeeklyProductionStandardDTOWithDetails(growth entity.StandardGrowthDetail
TargetHenHouseProduction: detail.TargetHenHouseProduction, TargetHenHouseProduction: detail.TargetHenHouseProduction,
TargetEggWeight: detail.TargetEggWeight, TargetEggWeight: detail.TargetEggWeight,
TargetEggMass: detail.TargetEggMass, TargetEggMass: detail.TargetEggMass,
StandardFCR: detail.StandardFCR,
} }
return WeeklyProductionStandardDTO{ return WeeklyProductionStandardDTO{
@@ -148,6 +150,7 @@ func ToEggProductionStandardDetailDTO(e entity.ProductionStandardDetail) EggProd
TargetHenHouseProduction: e.TargetHenHouseProduction, TargetHenHouseProduction: e.TargetHenHouseProduction,
TargetEggWeight: e.TargetEggWeight, TargetEggWeight: e.TargetEggWeight,
TargetEggMass: e.TargetEggMass, TargetEggMass: e.TargetEggMass,
StandardFCR: e.StandardFCR,
} }
} }
@@ -84,7 +84,6 @@ func (s productionStandardService) GetOne(c *fiber.Ctx, id uint) (*entity.Produc
return nil, fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found") return nil, fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get productionStandard by id: %+v", err)
return nil, err return nil, err
} }
return productionStandard, nil return productionStandard, nil
@@ -111,6 +110,7 @@ func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Crea
var createdStandard *entity.ProductionStandard var createdStandard *entity.ProductionStandard
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
standardRepoTx := repository.NewProductionStandardRepository(tx) standardRepoTx := repository.NewProductionStandardRepository(tx)
productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx) productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx)
standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx) standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx)
@@ -142,6 +142,7 @@ func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Crea
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction, TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight, TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass, TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
StandardFCR: detailReq.ProductionStandardDetails.StandardFCR,
} }
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
@@ -206,7 +207,6 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
nameExists, err := s.Repository.NameExists(c.Context(), *req.Name, &id) nameExists, err := s.Repository.NameExists(c.Context(), *req.Name, &id)
if err != nil { if err != nil {
s.Log.Errorf("Failed to check existing production standard: %+v", err)
return err return err
} }
if nameExists { if nameExists {
@@ -255,6 +255,7 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction, TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight, TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass, TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
StandardFCR: detailReq.ProductionStandardDetails.StandardFCR,
} }
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil { if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
@@ -283,7 +284,6 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to update production standard: %+v", err)
return nil, err return nil, err
} }
@@ -295,7 +295,6 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found") return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
} }
s.Log.Errorf("Failed to delete productionStandard: %+v", err)
return err return err
} }
return nil return nil
@@ -5,6 +5,7 @@ type ProductionStandardDetailItem struct {
TargetHenHouseProduction *float64 `json:"target_hen_house_production" validate:"omitempty,gte=0"` TargetHenHouseProduction *float64 `json:"target_hen_house_production" validate:"omitempty,gte=0"`
TargetEggWeight *float64 `json:"target_egg_weight" validate:"omitempty,gte=0"` TargetEggWeight *float64 `json:"target_egg_weight" validate:"omitempty,gte=0"`
TargetEggMass *float64 `json:"target_egg_mass" validate:"omitempty,gte=0"` TargetEggMass *float64 `json:"target_egg_mass" validate:"omitempty,gte=0"`
StandardFCR *float64 `json:"standard_fcr" validate:"omitempty,gte=0"`
} }
type StandardGrowthDetailItem struct { type StandardGrowthDetailItem struct {
@@ -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
@@ -1,14 +1,14 @@
package validation package validation
type Create struct { type Create struct {
FlockName string `json:"flock_name" validate:"required_strict"` FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"` ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"` ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
} }
type Query struct { type Query struct {
@@ -17,6 +17,7 @@ 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 CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
@@ -81,6 +82,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{}).
+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
}
+19 -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,12 +423,12 @@ 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"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
) )