mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
+6
@@ -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;
|
||||||
+58
@@ -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);
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_bws (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
avg_weight NUMERIC(8,2) NOT NULL,
|
||||||
|
qty NUMERIC(15,3) NOT NULL DEFAULT 1,
|
||||||
|
total_weight NUMERIC(10,3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_bws_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_bws_nonneg
|
||||||
|
CHECK (avg_weight >= 0 AND qty >= 0 AND total_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
|
||||||
|
ON recording_bws (recording_id);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP COLUMN IF EXISTS hand_day,
|
||||||
|
DROP COLUMN IF EXISTS hand_house,
|
||||||
|
DROP COLUMN IF EXISTS feed_intake,
|
||||||
|
DROP COLUMN IF EXISTS egg_mesh,
|
||||||
|
DROP COLUMN IF EXISTS egg_weight;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ALTER COLUMN weight TYPE NUMERIC(10,3) USING weight::NUMERIC(10,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||||
|
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD COLUMN IF NOT EXISTS hand_day NUMERIC(15,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS hand_house NUMERIC(15,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS feed_intake NUMERIC(15,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS egg_mesh NUMERIC(15,3),
|
||||||
|
ADD COLUMN IF NOT EXISTS egg_weight NUMERIC(15,3);
|
||||||
|
|
||||||
|
ALTER TABLE recordings
|
||||||
|
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
|
||||||
|
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||||
|
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
|
||||||
|
(hand_day IS NULL OR hand_day >= 0) AND
|
||||||
|
(hand_house IS NULL OR hand_house >= 0) AND
|
||||||
|
(feed_intake IS NULL OR feed_intake >= 0) AND
|
||||||
|
(egg_mesh IS NULL OR egg_mesh >= 0) AND
|
||||||
|
(egg_weight IS NULL OR egg_weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ALTER COLUMN weight TYPE NUMERIC(15,3) USING weight::NUMERIC(15,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||||
|
qty >= 0 AND
|
||||||
|
(weight IS NULL OR weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_bws_recording;
|
||||||
|
DROP TABLE IF EXISTS recording_bws;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectFlockKandangUniformity struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Uniformity float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
Week int `gorm:"not null"`
|
||||||
|
Cv float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
MeanUp float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
MeanDown float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
ProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UniformDate *time.Time `gorm:"type:timestamptz"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
|
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ProjectFlockKandangUniformity) TableName() string {
|
||||||
|
return "project_flock_kandang_uniformity"
|
||||||
|
}
|
||||||
@@ -13,11 +13,14 @@ type Recording struct {
|
|||||||
Day *int `gorm:"column:day"`
|
Day *int `gorm:"column:day"`
|
||||||
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
||||||
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
|
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
|
||||||
DailyGain *float64 `gorm:"column:daily_gain"`
|
|
||||||
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
|
|
||||||
CumIntake *int `gorm:"column:cum_intake"`
|
CumIntake *int `gorm:"column:cum_intake"`
|
||||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||||
|
HandDay *float64 `gorm:"column:hand_day"`
|
||||||
|
HandHouse *float64 `gorm:"column:hand_house"`
|
||||||
|
FeedIntake *float64 `gorm:"column:feed_intake"`
|
||||||
|
EggMesh *float64 `gorm:"column:egg_mesh"`
|
||||||
|
EggWeight *float64 `gorm:"column:egg_weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
@@ -25,10 +28,17 @@ type Recording struct {
|
|||||||
|
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
|
|
||||||
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
|
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
|
||||||
|
StandardHandDay *float64 `gorm:"-"`
|
||||||
|
StandardHandHouse *float64 `gorm:"-"`
|
||||||
|
StandardFeedIntake *float64 `gorm:"-"`
|
||||||
|
StandardMaxDepletion *float64 `gorm:"-"`
|
||||||
|
StandardEggMesh *float64 `gorm:"-"`
|
||||||
|
StandardEggWeight *float64 `gorm:"-"`
|
||||||
|
StandardFcr *float64 `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type RecordingBW struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
|
||||||
AvgWeight float64 `gorm:"column:avg_weight;not null"`
|
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
|
||||||
TotalWeight float64 `gorm:"column:total_weight;not null"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
|
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ const (
|
|||||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
||||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
||||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
||||||
|
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -218,12 +219,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
P_FinanceGetAll = "lti.finance.list"
|
P_Uniformities_GetAll = "lti.production.uniformity.list"
|
||||||
P_FinanceGetOne = "lti.finance.detail"
|
P_Uniformities_GetOne = "lti.production.uniformity.detail"
|
||||||
P_FinanceCreateOne = "lti.finance.create"
|
P_Uniformities_Verify = "lti.production.uniformity.verify"
|
||||||
P_FinanceUpdateOne = "lti.finance.update"
|
P_Uniformities_CreateOne = "lti.production.uniformity.create"
|
||||||
P_FinanceDeleteOne = "lti.finance.delete"
|
P_Uniformities_UpdateOne = "lti.production.uniformity.update"
|
||||||
P_FinanceApproval = "lti.finance.approve"
|
P_Uniformities_DeleteOne = "lti.production.uniformity.delete"
|
||||||
|
P_Uniformities_Approval = "lti.production.uniformity.approve"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -215,10 +215,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(activeProjectFlocks) == 0 {
|
if len(activeProjectFlocks) > 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "No active project flocks found for this location")
|
|
||||||
}
|
|
||||||
|
|
||||||
projectFlockIDs := make([]uint64, len(activeProjectFlocks))
|
projectFlockIDs := make([]uint64, len(activeProjectFlocks))
|
||||||
for i, pf := range activeProjectFlocks {
|
for i, pf := range activeProjectFlocks {
|
||||||
projectFlockIDs[i] = uint64(pf.Id)
|
projectFlockIDs[i] = uint64(pf.Id)
|
||||||
@@ -231,6 +228,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
jsonStr := string(projectFlockIdsJSON)
|
jsonStr := string(projectFlockIdsJSON)
|
||||||
projectFlockIdJSON = &jsonStr
|
projectFlockIdJSON = &jsonStr
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expense = &entity.Expense{
|
expense = &entity.Expense{
|
||||||
ReferenceNumber: referenceNumber,
|
ReferenceNumber: referenceNumber,
|
||||||
|
|||||||
+51
@@ -29,6 +29,8 @@ type ProductWarehouseRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
||||||
|
GetByProductWarehouseAndProjectFlockKandang(ctx context.Context, productId, warehouseId, projectFlockKandangId uint) (*entity.ProductWarehouse, error)
|
||||||
|
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -214,6 +216,31 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inUseIDs []uint
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.PurchaseItem{}).
|
||||||
|
Where("product_warehouse_id IN ?", emptyIDs).
|
||||||
|
Distinct().
|
||||||
|
Pluck("product_warehouse_id", &inUseIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(inUseIDs) > 0 {
|
||||||
|
inUse := make(map[uint]struct{}, len(inUseIDs))
|
||||||
|
for _, id := range inUseIDs {
|
||||||
|
inUse[id] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := make([]uint, 0, len(emptyIDs))
|
||||||
|
for _, id := range emptyIDs {
|
||||||
|
if _, exists := inUse[id]; !exists {
|
||||||
|
filtered = append(filtered, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyIDs = filtered
|
||||||
|
}
|
||||||
|
if len(emptyIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.DB().WithContext(ctx).
|
if err := r.DB().WithContext(ctx).
|
||||||
Model(&entity.PurchaseItem{}).
|
Model(&entity.PurchaseItem{}).
|
||||||
Where("product_warehouse_id IN ?", emptyIDs).
|
Where("product_warehouse_id IN ?", emptyIDs).
|
||||||
@@ -272,6 +299,30 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
return entity.Id, nil
|
return entity.Id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang(
|
||||||
|
ctx context.Context,
|
||||||
|
productId uint,
|
||||||
|
warehouseId uint,
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
) (*entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouse entity.ProductWarehouse
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id = ?", productId, warehouseId, projectFlockKandangId).
|
||||||
|
First(&productWarehouse).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &productWarehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Delete(&entity.ProductWarehouse{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||||
var productWarehouse entity.ProductWarehouse
|
var productWarehouse entity.ProductWarehouse
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error {
|
|||||||
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||||
projectFlockId := c.QueryInt("project_flock_id", 0)
|
projectFlockId := c.QueryInt("project_flock_id", 0)
|
||||||
kandangId := c.QueryInt("kandang_id", 0)
|
kandangId := c.QueryInt("kandang_id", 0)
|
||||||
|
withPopulation := c.QueryBool("withpopulation", false)
|
||||||
|
|
||||||
if projectFlockId == 0 || kandangId == 0 {
|
if projectFlockId == 0 || kandangId == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
||||||
@@ -280,6 +281,13 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||||
dtoResult.AvailableQuantity = float64(availableStock)
|
dtoResult.AvailableQuantity = float64(availableStock)
|
||||||
|
if withPopulation {
|
||||||
|
population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dtoResult.Population = &population
|
||||||
|
}
|
||||||
|
|
||||||
if dtoResult.ProjectFlock != nil {
|
if dtoResult.ProjectFlock != nil {
|
||||||
for i := range dtoResult.ProjectFlock.Kandangs {
|
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type ProjectFlockKandangDTO struct {
|
|||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
||||||
AvailableQuantity float64 `json:"available_quantity"`
|
AvailableQuantity float64 `json:"available_quantity"`
|
||||||
|
Population *float64 `json:"population,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
|
||||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -32,6 +33,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||||
|
projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db)
|
||||||
|
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||||
@@ -43,7 +46,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate)
|
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectflockRoutes(router, userService, projectflockService)
|
ProjectflockRoutes(router, userService, projectflockService)
|
||||||
|
|||||||
+18
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+15
@@ -27,6 +27,7 @@ type ProjectFlockKandangRepository interface {
|
|||||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||||
|
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
@@ -89,6 +90,20 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont
|
|||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return []uint{}, nil
|
||||||
|
}
|
||||||
|
var ids []uint
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||||
|
Pluck("id", &ids).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
||||||
var records []entity.ProjectFlockKandang
|
var records []entity.ProjectFlockKandang
|
||||||
var total int64
|
var total int64
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||||
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ type ProjectflockService interface {
|
|||||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||||
|
GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error)
|
||||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||||
@@ -54,6 +56,8 @@ type projectflockService struct {
|
|||||||
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||||
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
|
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
|
||||||
PivotRepo repository.ProjectFlockKandangRepository
|
PivotRepo repository.ProjectFlockKandangRepository
|
||||||
|
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||||
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,8 @@ func NewProjectflockService(
|
|||||||
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||||
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
|
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
|
||||||
nonstockRepo nonstockRepository.NonstockRepository,
|
nonstockRepo nonstockRepository.NonstockRepository,
|
||||||
|
populationRepo repository.ProjectFlockPopulationRepository,
|
||||||
|
recordingRepo recordingRepo.RecordingRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
|
|
||||||
@@ -86,7 +92,10 @@ func NewProjectflockService(
|
|||||||
NonstockRepo: nonstockRepo,
|
NonstockRepo: nonstockRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
ProjectBudgetRepo: projectBudgetRepo,
|
||||||
PivotRepo: pivotRepo,
|
PivotRepo: pivotRepo,
|
||||||
|
PopulationRepo: populationRepo,
|
||||||
|
RecordingRepo: recordingRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||||
}
|
}
|
||||||
@@ -419,6 +428,34 @@ func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fibe
|
|||||||
return pfk, availableQuantity, nil
|
return pfk, availableQuantity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) {
|
||||||
|
if s.PopulationRepo == nil {
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||||
|
}
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RecordingRepo != nil {
|
||||||
|
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||||
|
}
|
||||||
|
if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 {
|
||||||
|
return *latest.TotalChickQty, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||||
idStr = strings.TrimSpace(idStr)
|
idStr = strings.TrimSpace(idStr)
|
||||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||||
@@ -795,6 +832,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
|||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||||
}
|
}
|
||||||
|
if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,6 +860,23 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pfkIDs) > 0 {
|
||||||
|
pwRepo := s.ProductWarehouseRepo
|
||||||
|
if dbTransaction != nil {
|
||||||
|
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||||
|
} else if pwRepo == nil {
|
||||||
|
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
|
||||||
|
}
|
||||||
|
if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if resetStatus {
|
if resetStatus {
|
||||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
|
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||||
@@ -856,6 +913,81 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
|
|||||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := s.ProductWarehouseRepo
|
||||||
|
if dbTransaction != nil {
|
||||||
|
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||||
|
} else if pwRepo == nil {
|
||||||
|
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB())
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseRepo := s.WarehouseRepo
|
||||||
|
if dbTransaction != nil {
|
||||||
|
warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction)
|
||||||
|
} else if warehouseRepo == nil {
|
||||||
|
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := []utils.FlagType{
|
||||||
|
utils.FlagAyamAfkir,
|
||||||
|
utils.FlagAyamCulling,
|
||||||
|
utils.FlagAyamMati,
|
||||||
|
utils.FlagTelurPecah,
|
||||||
|
utils.FlagTelurUtuh,
|
||||||
|
}
|
||||||
|
|
||||||
|
productIDs := make(map[utils.FlagType]uint, len(flags))
|
||||||
|
for _, flag := range flags {
|
||||||
|
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
productIDs[flag] = product.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
if record == nil || record.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range flags {
|
||||||
|
productID := productIDs[flag]
|
||||||
|
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
|
||||||
|
continue
|
||||||
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPW := entity.ProductWarehouse{
|
||||||
|
ProductId: productID,
|
||||||
|
WarehouseId: warehouse.Id,
|
||||||
|
ProjectFlockKandangId: &record.Id,
|
||||||
|
Quantity: 0,
|
||||||
|
}
|
||||||
|
if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
|
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -22,11 +22,21 @@ type RecordingRelationDTO struct {
|
|||||||
ProjectFlockCategory string `json:"project_flock_category"`
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||||
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||||
DailyGain float64 `json:"daily_gain"`
|
|
||||||
AvgDailyGain float64 `json:"avg_daily_gain"`
|
|
||||||
CumIntake int `json:"cum_intake"`
|
CumIntake int `json:"cum_intake"`
|
||||||
FcrValue float64 `json:"fcr_value"`
|
FcrValue float64 `json:"fcr_value"`
|
||||||
TotalChickQty float64 `json:"total_chick_qty"`
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
|
HandDay float64 `json:"hand_day"`
|
||||||
|
HandHouse float64 `json:"hand_house"`
|
||||||
|
FeedIntake float64 `json:"feed_intake"`
|
||||||
|
EggMesh float64 `json:"egg_mesh"`
|
||||||
|
EggWeight float64 `json:"egg_weight"`
|
||||||
|
StandardHandDay *float64 `json:"hand_day_std,omitempty"`
|
||||||
|
StandardHandHouse *float64 `json:"hand_house_std,omitempty"`
|
||||||
|
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
|
||||||
|
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
|
||||||
|
StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"`
|
||||||
|
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
|
||||||
|
StandardFcr *float64 `json:"fcr_std,omitempty"`
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +49,11 @@ type RecordingListDTO struct {
|
|||||||
|
|
||||||
type RecordingDetailDTO struct {
|
type RecordingDetailDTO struct {
|
||||||
RecordingListDTO
|
RecordingListDTO
|
||||||
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
|
|
||||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||||
Stocks []RecordingStockDTO `json:"stocks"`
|
Stocks []RecordingStockDTO `json:"stocks"`
|
||||||
Eggs []RecordingEggDTO `json:"eggs"`
|
Eggs []RecordingEggDTO `json:"eggs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingBodyWeightDTO struct {
|
|
||||||
AvgWeight float64 `json:"avg_weight"`
|
|
||||||
Qty float64 `json:"qty"`
|
|
||||||
TotalWeight float64 `json:"total_weight"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RecordingDepletionDTO struct {
|
type RecordingDepletionDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
@@ -88,11 +91,14 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
day int
|
day int
|
||||||
totalDepletionQty float64
|
totalDepletionQty float64
|
||||||
cumDepletionRate float64
|
cumDepletionRate float64
|
||||||
dailyGain float64
|
|
||||||
avgDailyGain float64
|
|
||||||
cumIntake int
|
cumIntake int
|
||||||
fcrValue float64
|
fcrValue float64
|
||||||
totalChickQty float64
|
totalChickQty float64
|
||||||
|
handDay float64
|
||||||
|
handHouse float64
|
||||||
|
feedIntake float64
|
||||||
|
eggMesh float64
|
||||||
|
eggWeight float64
|
||||||
)
|
)
|
||||||
|
|
||||||
if e.Day != nil {
|
if e.Day != nil {
|
||||||
@@ -104,12 +110,6 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
if e.CumDepletionRate != nil {
|
if e.CumDepletionRate != nil {
|
||||||
cumDepletionRate = *e.CumDepletionRate
|
cumDepletionRate = *e.CumDepletionRate
|
||||||
}
|
}
|
||||||
if e.DailyGain != nil {
|
|
||||||
dailyGain = *e.DailyGain
|
|
||||||
}
|
|
||||||
if e.AvgDailyGain != nil {
|
|
||||||
avgDailyGain = *e.AvgDailyGain
|
|
||||||
}
|
|
||||||
if e.CumIntake != nil {
|
if e.CumIntake != nil {
|
||||||
cumIntake = *e.CumIntake
|
cumIntake = *e.CumIntake
|
||||||
}
|
}
|
||||||
@@ -119,6 +119,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
if e.TotalChickQty != nil {
|
if e.TotalChickQty != nil {
|
||||||
totalChickQty = *e.TotalChickQty
|
totalChickQty = *e.TotalChickQty
|
||||||
}
|
}
|
||||||
|
if e.HandDay != nil {
|
||||||
|
handDay = *e.HandDay
|
||||||
|
}
|
||||||
|
if e.HandHouse != nil {
|
||||||
|
handHouse = *e.HandHouse
|
||||||
|
}
|
||||||
|
if e.FeedIntake != nil {
|
||||||
|
feedIntake = *e.FeedIntake
|
||||||
|
}
|
||||||
|
if e.EggMesh != nil {
|
||||||
|
eggMesh = *e.EggMesh
|
||||||
|
}
|
||||||
|
if e.EggWeight != nil {
|
||||||
|
eggWeight = *e.EggWeight
|
||||||
|
}
|
||||||
|
|
||||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
category := e.ProjectFlockKandang.ProjectFlock.Category
|
category := e.ProjectFlockKandang.ProjectFlock.Category
|
||||||
@@ -139,11 +154,21 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
ProjectFlockCategory: projectFlockCategory,
|
ProjectFlockCategory: projectFlockCategory,
|
||||||
TotalDepletionQty: totalDepletionQty,
|
TotalDepletionQty: totalDepletionQty,
|
||||||
CumDepletionRate: cumDepletionRate,
|
CumDepletionRate: cumDepletionRate,
|
||||||
DailyGain: dailyGain,
|
|
||||||
AvgDailyGain: avgDailyGain,
|
|
||||||
CumIntake: cumIntake,
|
CumIntake: cumIntake,
|
||||||
FcrValue: fcrValue,
|
FcrValue: fcrValue,
|
||||||
TotalChickQty: totalChickQty,
|
TotalChickQty: totalChickQty,
|
||||||
|
HandDay: handDay,
|
||||||
|
HandHouse: handHouse,
|
||||||
|
FeedIntake: feedIntake,
|
||||||
|
EggMesh: eggMesh,
|
||||||
|
EggWeight: eggWeight,
|
||||||
|
StandardHandDay: e.StandardHandDay,
|
||||||
|
StandardHandHouse: e.StandardHandHouse,
|
||||||
|
StandardFeedIntake: e.StandardFeedIntake,
|
||||||
|
StandardMaxDepletion: e.StandardMaxDepletion,
|
||||||
|
StandardEggMesh: e.StandardEggMesh,
|
||||||
|
StandardEggWeight: e.StandardEggWeight,
|
||||||
|
StandardFcr: e.StandardFcr,
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,25 +208,12 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
|||||||
|
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: listDTO,
|
RecordingListDTO: listDTO,
|
||||||
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
|
||||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
Eggs: eggs,
|
Eggs: eggs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO {
|
|
||||||
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
|
|
||||||
for i, bw := range bodyWeights {
|
|
||||||
result[i] = RecordingBodyWeightDTO{
|
|
||||||
AvgWeight: bw.AvgWeight,
|
|
||||||
Qty: bw.Qty,
|
|
||||||
TotalWeight: bw.TotalWeight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
|
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
|
||||||
result := make([]RecordingDepletionDTO, len(depletions))
|
result := make([]RecordingDepletionDTO, len(depletions))
|
||||||
for i, d := range depletions {
|
for i, d := range depletions {
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ type RecordingRepository interface {
|
|||||||
repository.BaseRepository[entity.Recording]
|
repository.BaseRepository[entity.Recording]
|
||||||
|
|
||||||
WithRelations(db *gorm.DB) *gorm.DB
|
WithRelations(db *gorm.DB) *gorm.DB
|
||||||
|
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||||
|
|
||||||
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
|
|
||||||
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
|
|
||||||
|
|
||||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||||
@@ -41,10 +39,11 @@ type RecordingRepository interface {
|
|||||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
|
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
|
||||||
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||||
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||||
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
|
GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error)
|
||||||
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
||||||
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
|
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
||||||
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
||||||
|
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||||
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
||||||
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
|
||||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||||
@@ -66,7 +65,6 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
Preload("ProjectFlockKandang").
|
Preload("ProjectFlockKandang").
|
||||||
Preload("ProjectFlockKandang.ProjectFlock").
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
Preload("BodyWeights").
|
|
||||||
Preload("Depletions").
|
Preload("Depletions").
|
||||||
Preload("Depletions.ProductWarehouse").
|
Preload("Depletions.ProductWarehouse").
|
||||||
Preload("Depletions.ProductWarehouse.Product").
|
Preload("Depletions.ProductWarehouse.Product").
|
||||||
@@ -81,6 +79,27 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Eggs.ProductWarehouse.Warehouse")
|
Preload("Eggs.ProductWarehouse.Warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) {
|
||||||
|
if projectFlockKandangId == 0 {
|
||||||
|
return nil, errors.New("project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var record entity.Recording
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||||
|
Order("record_datetime DESC").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(1).
|
||||||
|
Find(&record).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) || record.Id == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &record, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||||
var days []int
|
var days []int
|
||||||
if err := tx.Model(&entity.Recording{}).
|
if err := tx.Model(&entity.Recording{}).
|
||||||
@@ -92,17 +111,6 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
|
|||||||
return nextRecordingDay(days), nil
|
return nextRecordingDay(days), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
|
|
||||||
if len(bodyWeights) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tx.Create(&bodyWeights).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
|
|
||||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||||
if len(stocks) == 0 {
|
if len(stocks) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -271,21 +279,18 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang
|
|||||||
return int64(math.Round(total)), nil
|
return int64(math.Round(total)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) {
|
||||||
var result struct {
|
if projectFlockKandangId == 0 {
|
||||||
TotalWeight float64
|
|
||||||
TotalQty float64
|
|
||||||
}
|
|
||||||
if err := tx.Model(&entity.RecordingBW{}).
|
|
||||||
Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
|
||||||
Where("recording_id = ?", recordingID).
|
|
||||||
Scan(&result).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if result.TotalQty == 0 {
|
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
return result.TotalWeight / result.TotalQty, nil
|
|
||||||
|
var result float64
|
||||||
|
err := tx.
|
||||||
|
Table("project_chickins").
|
||||||
|
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
|
||||||
|
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
|
Scan(&result).Error
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
@@ -322,22 +327,48 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error) {
|
||||||
|
if recordingID == 0 {
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
FcrID uint
|
TotalQty float64
|
||||||
|
TotalWeightGrams float64
|
||||||
}
|
}
|
||||||
if err := tx.Table("project_flock_kandangs").
|
err = tx.
|
||||||
Select("project_flocks.fcr_id AS fcr_id").
|
Table("recording_eggs").
|
||||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(recording_eggs.qty * COALESCE(recording_eggs.weight, 0)), 0) AS total_weight_grams").
|
||||||
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
Where("recording_eggs.recording_id = ?", recordingID).
|
||||||
Scan(&result).Error; err != nil {
|
Scan(&result).Error
|
||||||
return 0, err
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
return result.FcrID, nil
|
return result.TotalQty, result.TotalWeightGrams, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang(
|
||||||
if fcrId == 0 {
|
tx *gorm.DB,
|
||||||
|
projectFlockKandangId uint,
|
||||||
|
recordTime time.Time,
|
||||||
|
) (float64, error) {
|
||||||
|
if projectFlockKandangId == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result float64
|
||||||
|
err := tx.
|
||||||
|
Table("recording_eggs").
|
||||||
|
Select("COALESCE(SUM(recording_eggs.qty), 0)").
|
||||||
|
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
|
||||||
|
Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||||
|
Where("recordings.record_datetime <= ?", recordTime).
|
||||||
|
Scan(&result).Error
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||||
|
if fcrId == 0 || currentWeightKg <= 0 {
|
||||||
return 0, false, nil
|
return 0, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,49 +391,12 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint
|
|||||||
return 0, false, err
|
return 0, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
weight := standard.Weight
|
return standard.FcrNumber, true, nil
|
||||||
if weight > 10 {
|
|
||||||
return weight / 1000, true, nil
|
|
||||||
}
|
|
||||||
return weight, true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
|
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
|
||||||
if projectFlockID == 0 {
|
// Body-weight tracking is removed; keep stub for report compatibility.
|
||||||
return 0, 0, nil
|
return 0, 0, nil
|
||||||
}
|
|
||||||
|
|
||||||
totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actualQty := totalChickinQty - totalDepletion
|
|
||||||
|
|
||||||
avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWeight = actualQty * avgWeight
|
|
||||||
|
|
||||||
return totalWeight, actualQty, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
|
||||||
var result float64
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Table("project_chickins").
|
|
||||||
Select("COALESCE(SUM(project_chickins.usage_qty), 0)").
|
|
||||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
|
|
||||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
|
||||||
Scan(&result).Error
|
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
||||||
@@ -418,16 +412,8 @@ func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
||||||
var result float64
|
// Body-weight tracking is removed; keep stub for report compatibility.
|
||||||
err := r.DB().WithContext(ctx).
|
return 0, nil
|
||||||
Table("recording_bws").
|
|
||||||
Select("COALESCE(AVG(recording_bws.avg_weight), 0)").
|
|
||||||
Joins("JOIN recordings ON recordings.id = recording_bws.recording_id").
|
|
||||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
|
|
||||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
|
||||||
Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID).
|
|
||||||
Scan(&result).Error
|
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
@@ -121,6 +122,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
if err := s.attachLatestApprovals(c.Context(), recordings); err != nil {
|
if err := s.attachLatestApprovals(c.Context(), recordings); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
if err := s.attachProductionStandards(c.Context(), recordings); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
return recordings, total, nil
|
return recordings, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +142,9 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
|||||||
if err := s.attachLatestApproval(c.Context(), recording); err != nil {
|
if err := s.attachLatestApproval(c.Context(), recording); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := s.attachProductionStandard(c.Context(), recording); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return recording, nil
|
return recording, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +240,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedBodyWeights := recordingutil.MapBodyWeights(createdRecording.Id, req.BodyWeights)
|
|
||||||
if err := s.Repository.CreateBodyWeights(tx, mappedBodyWeights); err != nil {
|
|
||||||
s.Log.Errorf("Failed to persist body weights: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
@@ -261,7 +262,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedEggs)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -291,7 +292,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
|
if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,12 +312,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
recordingEntity = recording
|
recordingEntity = recording
|
||||||
|
|
||||||
hasBodyChanges := req.BodyWeights != nil
|
|
||||||
hasStockChanges := req.Stocks != nil
|
hasStockChanges := req.Stocks != nil
|
||||||
hasDepletionChanges := req.Depletions != nil
|
hasDepletionChanges := req.Depletions != nil
|
||||||
hasEggChanges := req.Eggs != nil
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,17 +346,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasBodyChanges {
|
|
||||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
|
||||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.Repository.CreateBodyWeights(tx, recordingutil.MapBodyWeights(recordingEntity.Id, req.BodyWeights)); err != nil {
|
|
||||||
s.Log.Errorf("Failed to update body weights: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -402,7 +391,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil, nil, nil)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(existingDepletions, mappedDepletions, nil, nil)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -426,13 +415,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, nil, nil, existingEggs, mappedEggs)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingEggs, mappedEggs)); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
if hasStockChanges || hasDepletionChanges {
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -596,7 +585,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil {
|
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldEggs, nil)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,7 +713,6 @@ func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
|
|
||||||
func buildWarehouseDeltas(
|
func buildWarehouseDeltas(
|
||||||
oldDepletions, newDepletions []entity.RecordingDepletion,
|
oldDepletions, newDepletions []entity.RecordingDepletion,
|
||||||
oldStocks, newStocks []entity.RecordingStock,
|
|
||||||
oldEggs, newEggs []entity.RecordingEgg,
|
oldEggs, newEggs []entity.RecordingEgg,
|
||||||
) map[uint]float64 {
|
) map[uint]float64 {
|
||||||
deltas := make(map[uint]float64)
|
deltas := make(map[uint]float64)
|
||||||
@@ -775,7 +763,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
|
|
||||||
var prevCumDepletionQty float64
|
var prevCumDepletionQty float64
|
||||||
var prevCumIntake float64
|
var prevCumIntake float64
|
||||||
var prevAvgWeight float64
|
|
||||||
if prevRecording != nil {
|
if prevRecording != nil {
|
||||||
if prevRecording.TotalDepletionQty != nil {
|
if prevRecording.TotalDepletionQty != nil {
|
||||||
prevCumDepletionQty = *prevRecording.TotalDepletionQty
|
prevCumDepletionQty = *prevRecording.TotalDepletionQty
|
||||||
@@ -783,10 +770,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
if prevRecording.CumIntake != nil {
|
if prevRecording.CumIntake != nil {
|
||||||
prevCumIntake = float64(*prevRecording.CumIntake)
|
prevCumIntake = float64(*prevRecording.CumIntake)
|
||||||
}
|
}
|
||||||
prevAvgWeight, err = s.Repository.GetAverageBodyWeight(tx, prevRecording.Id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
|
totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
|
||||||
@@ -794,20 +777,25 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
return fmt.Errorf("getTotalChick: %w", err)
|
return fmt.Errorf("getTotalChick: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAvgWeight, err := s.Repository.GetAverageBodyWeight(tx, recording.Id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
|
usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAvgGrams := recordingutil.ToGrams(currentAvgWeight)
|
totalEggQty, totalEggWeightGrams, err := s.Repository.GetEggSummaryByRecording(tx, recording.Id)
|
||||||
currentAvgKg := recordingutil.GramsToKg(currentAvgGrams)
|
if err != nil {
|
||||||
prevAvgGrams := recordingutil.ToGrams(prevAvgWeight)
|
return fmt.Errorf("getEggSummaryByRecording: %w", err)
|
||||||
prevAvgKg := recordingutil.GramsToKg(prevAvgGrams)
|
}
|
||||||
|
|
||||||
|
cumulativeEggQty, err := s.Repository.GetCumulativeEggQtyByProjectFlockKandang(tx, recording.ProjectFlockKandangId, recording.RecordDatetime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getCumulativeEggQtyByProjectFlockKandang: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialChickin, err := s.Repository.GetTotalChickinByProjectFlockKandang(tx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getTotalChickinByProjectFlockKandang: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
currentDepletion := float64(totalDepletionQty)
|
currentDepletion := float64(totalDepletionQty)
|
||||||
cumDepletionQty := prevCumDepletionQty + currentDepletion
|
cumDepletionQty := prevCumDepletionQty + currentDepletion
|
||||||
@@ -840,24 +828,64 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
recording.CumDepletionRate = nil
|
recording.CumDepletionRate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentAvgGrams > 0 && prevAvgGrams > 0 {
|
var feedIntake float64
|
||||||
dailyGainKg := (currentAvgGrams - prevAvgGrams) / 1000
|
if remainingChick > 0 && usageInGrams > 0 {
|
||||||
updates["daily_gain"] = dailyGainKg
|
feedIntake = (usageInGrams / remainingChick) * 1000
|
||||||
recording.DailyGain = &dailyGainKg
|
updates["feed_intake"] = feedIntake
|
||||||
|
recording.FeedIntake = &feedIntake
|
||||||
} else {
|
} else {
|
||||||
dailyGainKg := 0.0
|
updates["feed_intake"] = gorm.Expr("NULL")
|
||||||
updates["daily_gain"] = dailyGainKg
|
recording.FeedIntake = nil
|
||||||
recording.DailyGain = &dailyGainKg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentAvgKg > 0 && remainingChick > 0 {
|
var handDay float64
|
||||||
avgDailyGain := (currentAvgKg - prevAvgKg) / remainingChick
|
if remainingChick > 0 && totalEggQty >= 0 {
|
||||||
updates["avg_daily_gain"] = avgDailyGain
|
handDay = (totalEggQty / remainingChick) * 100
|
||||||
recording.AvgDailyGain = &avgDailyGain
|
updates["hand_day"] = handDay
|
||||||
|
recording.HandDay = &handDay
|
||||||
} else {
|
} else {
|
||||||
avgDailyGain := 0.0
|
updates["hand_day"] = gorm.Expr("NULL")
|
||||||
updates["avg_daily_gain"] = avgDailyGain
|
recording.HandDay = nil
|
||||||
recording.AvgDailyGain = &avgDailyGain
|
}
|
||||||
|
|
||||||
|
var handHouse float64
|
||||||
|
if initialChickin > 0 && cumulativeEggQty >= 0 {
|
||||||
|
handHouse = cumulativeEggQty / initialChickin
|
||||||
|
updates["hand_house"] = handHouse
|
||||||
|
recording.HandHouse = &handHouse
|
||||||
|
} else {
|
||||||
|
updates["hand_house"] = gorm.Expr("NULL")
|
||||||
|
recording.HandHouse = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var eggMesh float64
|
||||||
|
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
||||||
|
eggMesh = (totalEggWeightGrams / remainingChick) * 1000
|
||||||
|
updates["egg_mesh"] = eggMesh
|
||||||
|
recording.EggMesh = &eggMesh
|
||||||
|
} else {
|
||||||
|
updates["egg_mesh"] = gorm.Expr("NULL")
|
||||||
|
recording.EggMesh = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var eggWeight float64
|
||||||
|
if totalEggQty > 0 && totalEggWeightGrams > 0 {
|
||||||
|
eggWeight = (totalEggWeightGrams / totalEggQty) * 1000
|
||||||
|
updates["egg_weight"] = eggWeight
|
||||||
|
recording.EggWeight = &eggWeight
|
||||||
|
} else {
|
||||||
|
updates["egg_weight"] = gorm.Expr("NULL")
|
||||||
|
recording.EggWeight = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fcrValue float64
|
||||||
|
if usageInGrams > 0 && totalEggWeightGrams > 0 {
|
||||||
|
fcrValue = totalEggWeightGrams / usageInGrams
|
||||||
|
updates["fcr_value"] = fcrValue
|
||||||
|
recording.FcrValue = &fcrValue
|
||||||
|
} else {
|
||||||
|
updates["fcr_value"] = gorm.Expr("NULL")
|
||||||
|
recording.FcrValue = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if usageInGrams > 0 && totalChick > 0 {
|
if usageInGrams > 0 && totalChick > 0 {
|
||||||
@@ -882,16 +910,6 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
recording.CumIntake = nil
|
recording.CumIntake = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if usageInGrams > 0 && currentAvgKg > 0 {
|
|
||||||
feedUsageKg := usageInGrams / 1000
|
|
||||||
fcrValue := feedUsageKg / currentAvgKg
|
|
||||||
updates["fcr_value"] = fcrValue
|
|
||||||
recording.FcrValue = &fcrValue
|
|
||||||
} else {
|
|
||||||
updates["fcr_value"] = gorm.Expr("NULL")
|
|
||||||
recording.FcrValue = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil {
|
if err := s.Repository.WithTx(tx).PatchOne(ctx, recording.Id, updates, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -997,6 +1015,104 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type productionStandardValues struct {
|
||||||
|
HandDay *float64
|
||||||
|
HandHouse *float64
|
||||||
|
FeedIntake *float64
|
||||||
|
MaxDepletion *float64
|
||||||
|
EggMesh *float64
|
||||||
|
EggWeight *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) attachProductionStandards(ctx context.Context, items []entity.Recording) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range items {
|
||||||
|
if err := s.attachProductionStandard(ctx, &items[i]); err != nil {
|
||||||
|
s.Log.Warnf("Unable to load production standard for recording %d: %+v", items[i].Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) attachProductionStandard(ctx context.Context, item *entity.Recording) error {
|
||||||
|
if item == nil || item.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if item.Day == nil || *item.Day <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId
|
||||||
|
if standardID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
week := ((int(*item.Day) - 1) / 7) + 1
|
||||||
|
if week <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category)
|
||||||
|
db := s.Repository.DB()
|
||||||
|
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
|
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||||
|
|
||||||
|
var standard productionStandardValues
|
||||||
|
var standardFcr *float64
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
detail, err := standardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if detail != nil {
|
||||||
|
standard.HandDay = detail.TargetHenDayProduction
|
||||||
|
standard.HandHouse = detail.TargetHenHouseProduction
|
||||||
|
standard.EggWeight = detail.TargetEggWeight
|
||||||
|
standard.EggMesh = detail.TargetEggMass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
growthDetail, err := growthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if growthDetail != nil {
|
||||||
|
standard.FeedIntake = growthDetail.FeedIntake
|
||||||
|
standard.MaxDepletion = growthDetail.MaxDepletion
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) && growthDetail.TargetMeanBw != nil && item.ProjectFlockKandang.ProjectFlock.FcrId > 0 {
|
||||||
|
targetWeight := *growthDetail.TargetMeanBw
|
||||||
|
if targetWeight > 10 {
|
||||||
|
targetWeight = targetWeight / 1000
|
||||||
|
}
|
||||||
|
if targetWeight > 0 {
|
||||||
|
fcrStd, ok, err := s.Repository.GetFcrStandardNumber(db, item.ProjectFlockKandang.ProjectFlock.FcrId, targetWeight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
standardFcr = &fcrStd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.StandardHandDay = standard.HandDay
|
||||||
|
item.StandardHandHouse = standard.HandHouse
|
||||||
|
item.StandardFeedIntake = standard.FeedIntake
|
||||||
|
item.StandardMaxDepletion = standard.MaxDepletion
|
||||||
|
item.StandardEggMesh = standard.EggMesh
|
||||||
|
item.StandardEggWeight = standard.EggWeight
|
||||||
|
item.StandardFcr = standardFcr
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func uniqueUintSlice(values []uint) []uint {
|
func uniqueUintSlice(values []uint) []uint {
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type (
|
type (
|
||||||
BodyWeight struct {
|
|
||||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
|
||||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
|
||||||
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Stock struct {
|
Stock struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||||
@@ -27,14 +21,12 @@ type (
|
|||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
BodyWeights []BodyWeight `json:"body_weights" validate:"dive"`
|
|
||||||
Stocks []Stock `json:"stocks" validate:"dive"`
|
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||||
Depletions []Depletion `json:"depletions" validate:"dive"`
|
Depletions []Depletion `json:"depletions" validate:"dive"`
|
||||||
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
|
||||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||||
|
|||||||
@@ -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 ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64Ptr(value float64) *float64 {
|
||||||
|
copy := value
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) {
|
||||||
|
if uniformityID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ApprovalRepo != nil {
|
||||||
|
if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil {
|
||||||
|
s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil {
|
||||||
|
s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueUintSlice(values []uint) []uint {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{}, len(values))
|
||||||
|
result := make([]uint, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
if v == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
||||||
|
weights := make([]float64, 0, len(rows))
|
||||||
|
details := make([]UniformityDetailItem, 0, len(rows))
|
||||||
|
hasRangeLabels := false
|
||||||
|
for idx, row := range rows {
|
||||||
|
if row.Weight <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := row.No
|
||||||
|
if id <= 0 {
|
||||||
|
id = idx + 1
|
||||||
|
}
|
||||||
|
weights = append(weights, row.Weight)
|
||||||
|
rangeLabel := strings.TrimSpace(row.Range)
|
||||||
|
if rangeLabel != "" {
|
||||||
|
upper := strings.ToUpper(rangeLabel)
|
||||||
|
if upper == "HIGH" || upper == "LOW" {
|
||||||
|
hasRangeLabels = true
|
||||||
|
}
|
||||||
|
rangeLabel = upper
|
||||||
|
}
|
||||||
|
details = append(details, UniformityDetailItem{
|
||||||
|
Id: id,
|
||||||
|
Weight: row.Weight,
|
||||||
|
Range: rangeLabel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := float64(len(weights))
|
||||||
|
if total == 0 {
|
||||||
|
return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum float64
|
||||||
|
for _, w := range weights {
|
||||||
|
sum += w
|
||||||
|
}
|
||||||
|
mean := sum / total
|
||||||
|
meanUpThreshold := roundToPrecision(mean*1.10, 3)
|
||||||
|
meanDownThreshold := roundToPrecision(mean*0.90, 3)
|
||||||
|
|
||||||
|
var uniformCount float64
|
||||||
|
for i := range details {
|
||||||
|
if hasRangeLabels {
|
||||||
|
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
|
||||||
|
details[i].Range = "Outside"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
details[i].Range = "Ideal"
|
||||||
|
uniformCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
w := details[i].Weight
|
||||||
|
if w > meanUpThreshold || w < meanDownThreshold {
|
||||||
|
details[i].Range = "Outside"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
details[i].Range = "Ideal"
|
||||||
|
uniformCount++
|
||||||
|
}
|
||||||
|
outsideCount := total - uniformCount
|
||||||
|
|
||||||
|
var cv float64
|
||||||
|
if mean > 0 && total > 1 {
|
||||||
|
stddevWeights := weights
|
||||||
|
stddevCount := float64(len(stddevWeights))
|
||||||
|
if stddevCount > 1 {
|
||||||
|
var stddevSum float64
|
||||||
|
for _, w := range stddevWeights {
|
||||||
|
stddevSum += w
|
||||||
|
}
|
||||||
|
stddevMean := stddevSum / stddevCount
|
||||||
|
var sumSquares float64
|
||||||
|
for _, w := range stddevWeights {
|
||||||
|
diff := w - stddevMean
|
||||||
|
sumSquares += diff * diff
|
||||||
|
}
|
||||||
|
stddev := math.Sqrt(sumSquares / (stddevCount - 1))
|
||||||
|
cv = (stddev / mean) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uniformity := (uniformCount / total) * 100
|
||||||
|
|
||||||
|
return UniformityCalculation{
|
||||||
|
ChickQtyOfWeight: total,
|
||||||
|
MeanWeight: roundToPrecision(mean, 0),
|
||||||
|
MeanDown: roundToPrecision(mean*0.90, 0),
|
||||||
|
MeanUp: roundToPrecision(mean*1.10, 0),
|
||||||
|
UniformQty: uniformCount,
|
||||||
|
OutsideQty: outsideCount,
|
||||||
|
Uniformity: roundToPrecision(uniformity, 0),
|
||||||
|
Cv: roundToPrecision(cv, 1),
|
||||||
|
Details: details,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundToPrecision(value float64, precision int) float64 {
|
||||||
|
if precision < 0 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
scale := math.Pow10(precision)
|
||||||
|
scaled := value * scale
|
||||||
|
fraction := scaled - math.Floor(scaled)
|
||||||
|
if fraction >= 0.5 {
|
||||||
|
return math.Ceil(scaled) / scale
|
||||||
|
}
|
||||||
|
return math.Floor(scaled) / scale
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Date string `form:"date" validate:"required"`
|
||||||
|
ProjectFlockKandangId uint `form:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
|
Week int `form:"week" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Date *string `json:"date,omitempty" form:"date" validate:"omitempty"`
|
||||||
|
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty" form:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
|
Week *int `json:"week,omitempty" form:"week" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
|
Week int `query:"week" validate:"omitempty,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadExcelRequest struct {
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Approve struct {
|
||||||
|
Action string `json:"action" validate:"required_strict"`
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseIDParam(c *fiber.Ctx, name string) (uint, error) {
|
||||||
|
raw := strings.TrimSpace(c.Params(name))
|
||||||
|
if raw == "" {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
return uint(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseQuery(c *fiber.Ctx) (*Query, error) {
|
||||||
|
query := &Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
Week: c.QueryInt("week", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) {
|
||||||
|
date := strings.TrimSpace(c.FormValue("date"))
|
||||||
|
if date == "" {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "date is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
|
||||||
|
if projectFlockKandangIDStr == "" {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
|
||||||
|
if err != nil || projectFlockKandangID <= 0 {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
weekStr := strings.TrimSpace(c.FormValue("week"))
|
||||||
|
if weekStr == "" {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
week, err := strconv.Atoi(weekStr)
|
||||||
|
if err != nil || week <= 0 {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := c.FormFile("document")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Create{
|
||||||
|
Date: date,
|
||||||
|
ProjectFlockKandangId: uint(projectFlockKandangID),
|
||||||
|
Week: week,
|
||||||
|
}, file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseUpdate(c *fiber.Ctx) (*Update, *multipart.FileHeader, error) {
|
||||||
|
contentType := strings.ToLower(c.Get("Content-Type"))
|
||||||
|
if strings.Contains(contentType, "multipart/form-data") {
|
||||||
|
req := &Update{}
|
||||||
|
|
||||||
|
date := strings.TrimSpace(c.FormValue("date"))
|
||||||
|
if date != "" {
|
||||||
|
req.Date = &date
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id"))
|
||||||
|
if projectFlockKandangIDStr != "" {
|
||||||
|
projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr)
|
||||||
|
if err != nil || projectFlockKandangID <= 0 {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is invalid")
|
||||||
|
}
|
||||||
|
idCopy := uint(projectFlockKandangID)
|
||||||
|
req.ProjectFlockKandangId = &idCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
weekStr := strings.TrimSpace(c.FormValue("week"))
|
||||||
|
if weekStr != "" {
|
||||||
|
week, err := strconv.Atoi(weekStr)
|
||||||
|
if err != nil || week <= 0 {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is invalid")
|
||||||
|
}
|
||||||
|
req.Week = &week
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := c.FormFile("document")
|
||||||
|
if err != nil {
|
||||||
|
file = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(Update)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
return req, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseUploadFiles(c *fiber.Ctx) ([]*multipart.FileHeader, error) {
|
||||||
|
file, err := c.FormFile("document")
|
||||||
|
if err != nil || file == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*multipart.FileHeader{file}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseApprove(c *fiber.Ctx) (*Approve, error) {
|
||||||
|
req := new(Approve)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -86,7 +87,6 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
|
|||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := ctrl.service.CreateOne(c, req)
|
result, err := ctrl.service.CreateOne(c, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -161,10 +161,26 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := new(validation.ReceivePurchaseRequest)
|
req := new(validation.ReceivePurchaseRequest)
|
||||||
if err := c.BodyParser(req); err != nil {
|
form, err := c.MultipartForm()
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||||
|
}
|
||||||
|
req.Action = c.FormValue("action")
|
||||||
|
if notes := strings.TrimSpace(c.FormValue("notes")); notes != "" {
|
||||||
|
req.Notes = ¬es
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemsJSON := c.FormValue("items")
|
||||||
|
if strings.TrimSpace(itemsJSON) != "" {
|
||||||
|
if err := json.Unmarshal([]byte(itemsJSON), &req.Items); err != nil {
|
||||||
|
var singleItem validation.ReceivePurchaseItemRequest
|
||||||
|
if err := json.Unmarshal([]byte(itemsJSON), &singleItem); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid items JSON")
|
||||||
|
}
|
||||||
|
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.TravelDocuments = form.File["documents"]
|
||||||
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
approvalService,
|
approvalService,
|
||||||
expenseBridge,
|
expenseBridge,
|
||||||
fifoService,
|
fifoService,
|
||||||
|
documentSvc,
|
||||||
)
|
)
|
||||||
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|||||||
@@ -310,9 +310,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cnt == 1 {
|
if cnt == 1 {
|
||||||
if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
|
||||||
}
|
|
||||||
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -332,7 +329,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
"price": pricePerItem,
|
"price": pricePerItem,
|
||||||
"notes": note,
|
"notes": note,
|
||||||
"nonstock_id": newNonstockID,
|
"nonstock_id": newNonstockID,
|
||||||
"kandang_id": uint64(*item.Warehouse.KandangId),
|
}
|
||||||
|
if item.Warehouse != nil && item.Warehouse.KandangId != nil && *item.Warehouse.KandangId != 0 {
|
||||||
|
updateBody["kandang_id"] = uint64(*item.Warehouse.KandangId)
|
||||||
}
|
}
|
||||||
if err := b.db.WithContext(ctx).
|
if err := b.db.WithContext(ctx).
|
||||||
Model(&entity.ExpenseNonstock{}).
|
Model(&entity.ExpenseNonstock{}).
|
||||||
@@ -395,9 +394,13 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
if kandangID != nil {
|
if kandangID != nil {
|
||||||
updateBody["kandang_id"] = uint64(*kandangID)
|
updateBody["kandang_id"] = uint64(*kandangID)
|
||||||
|
} else {
|
||||||
|
updateBody["kandang_id"] = nil
|
||||||
}
|
}
|
||||||
if projectFK != nil {
|
if projectFK != nil {
|
||||||
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
|
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
|
||||||
|
} else {
|
||||||
|
updateBody["project_flock_kandang_id"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.db.WithContext(ctx).
|
if err := b.db.WithContext(ctx).
|
||||||
@@ -550,10 +553,9 @@ func (b *expenseBridge) createExpenseViaService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
kandangID := items[0].kandangID
|
kandangID := items[0].kandangID
|
||||||
if kandangID == nil || *kandangID == 0 {
|
var locationID uint64
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
var expenseKandangID *uint64
|
||||||
}
|
if kandangID != nil && *kandangID != 0 {
|
||||||
|
|
||||||
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
|
kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Select("id, location_id")
|
return db.Select("id, location_id")
|
||||||
})
|
})
|
||||||
@@ -563,6 +565,16 @@ func (b *expenseBridge) createExpenseViaService(
|
|||||||
if kandang == nil {
|
if kandang == nil {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
|
||||||
}
|
}
|
||||||
|
locationID = uint64(kandang.LocationId)
|
||||||
|
id := uint64(*kandangID)
|
||||||
|
expenseKandangID = &id
|
||||||
|
} else {
|
||||||
|
warehouse := items[0].item.Warehouse
|
||||||
|
if warehouse == nil || warehouse.LocationId == nil || *warehouse.LocationId == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse location is required for expense")
|
||||||
|
}
|
||||||
|
locationID = uint64(*warehouse.LocationId)
|
||||||
|
}
|
||||||
|
|
||||||
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
||||||
for _, gi := range items {
|
for _, gi := range items {
|
||||||
@@ -584,9 +596,9 @@ func (b *expenseBridge) createExpenseViaService(
|
|||||||
TransactionDate: utils.FormatDate(expenseDate),
|
TransactionDate: utils.FormatDate(expenseDate),
|
||||||
Category: "BOP",
|
Category: "BOP",
|
||||||
SupplierID: uint64(supplierID),
|
SupplierID: uint64(supplierID),
|
||||||
LocationID: uint64(kandang.LocationId),
|
LocationID: locationID,
|
||||||
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
||||||
KandangID: func() *uint64 { id := uint64(*kandangID); return &id }(),
|
KandangID: expenseKandangID,
|
||||||
CostItems: costItems,
|
CostItems: costItems,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ type purchaseService struct {
|
|||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
ExpenseBridge PurchaseExpenseBridge
|
ExpenseBridge PurchaseExpenseBridge
|
||||||
FifoSvc commonSvc.FifoService
|
FifoSvc commonSvc.FifoService
|
||||||
|
DocumentSvc commonSvc.DocumentService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ func NewPurchaseService(
|
|||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
expenseBridge PurchaseExpenseBridge,
|
expenseBridge PurchaseExpenseBridge,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoSvc commonSvc.FifoService,
|
||||||
|
documentSvc commonSvc.DocumentService,
|
||||||
) PurchaseService {
|
) PurchaseService {
|
||||||
return &purchaseService{
|
return &purchaseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -89,6 +92,7 @@ func NewPurchaseService(
|
|||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
ExpenseBridge: expenseBridge,
|
ExpenseBridge: expenseBridge,
|
||||||
FifoSvc: fifoSvc,
|
FifoSvc: fifoSvc,
|
||||||
|
DocumentSvc: documentSvc,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,10 +250,12 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
||||||
return nil, nil, utils.Internal("Failed to get warehouse")
|
return nil, nil, utils.Internal("Failed to get warehouse")
|
||||||
}
|
}
|
||||||
|
var pfkID *uint
|
||||||
|
isKandang := strings.EqualFold(strings.TrimSpace(warehouse.Type), "KANDANG")
|
||||||
|
if isKandang {
|
||||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name))
|
return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name))
|
||||||
}
|
}
|
||||||
var pfkID *uint
|
|
||||||
if s.ProjectFlockKandangRepo != nil {
|
if s.ProjectFlockKandangRepo != nil {
|
||||||
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
||||||
if pfk.ClosedAt != nil {
|
if pfk.ClosedAt != nil {
|
||||||
@@ -264,6 +270,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
return nil, nil, utils.Internal("Failed to validate project flock")
|
return nil, nil, utils.Internal("Failed to validate project flock")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
warehouseCache[id] = warehouse
|
warehouseCache[id] = warehouse
|
||||||
return warehouse, pfkID, nil
|
return warehouse, pfkID, nil
|
||||||
@@ -612,9 +619,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
action, err := parseApprovalActionInput(req.Action)
|
action, err := parseApprovalActionInput(req.Action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -661,6 +666,30 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved && len(req.TravelDocuments) > 0 {
|
||||||
|
if len(req.TravelDocuments) > len(req.Items) {
|
||||||
|
return nil, utils.BadRequest("Travel documents exceed total receiving items")
|
||||||
|
}
|
||||||
|
for idx, file := range req.TravelDocuments {
|
||||||
|
if file == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx >= len(req.Items) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
itemID := req.Items[idx].PurchaseItemID
|
||||||
|
if itemID == 0 {
|
||||||
|
return nil, utils.BadRequest("Purchase item id is required for travel document upload")
|
||||||
|
}
|
||||||
|
uploadedURL, err := s.uploadTravelDocument(ctx, actorID, itemID, file)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to upload travel document for item %d: %+v", itemID, err)
|
||||||
|
return nil, utils.Internal("Failed to upload travel document")
|
||||||
|
}
|
||||||
|
req.Items[idx].TravelDocumentPath = &uploadedURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
||||||
for i := range purchase.Items {
|
for i := range purchase.Items {
|
||||||
itemMap[purchase.Items[i].Id] = &purchase.Items[i]
|
itemMap[purchase.Items[i].Id] = &purchase.Items[i]
|
||||||
@@ -804,17 +833,9 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
for _, prep := range prepared {
|
for _, prep := range prepared {
|
||||||
item := prep.item
|
item := prep.item
|
||||||
|
|
||||||
var oldPWID *uint
|
|
||||||
if item.ProductWarehouseId != nil {
|
|
||||||
idCopy := uint(*item.ProductWarehouseId)
|
|
||||||
oldPWID = &idCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
var newPWID *uint
|
var newPWID *uint
|
||||||
clearPW := false
|
|
||||||
|
|
||||||
// Always ensure PW when qty > 0 so stockable has target.
|
// Always ensure PW after receiving so linkage stays stable.
|
||||||
if prep.receivedQty > 0 {
|
|
||||||
pwID, err := pwRepoTx.EnsureProductWarehouse(
|
pwID, err := pwRepoTx.EnsureProductWarehouse(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
uint(item.ProductId),
|
uint(item.ProductId),
|
||||||
@@ -826,10 +847,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newPWID = &pwID
|
newPWID = &pwID
|
||||||
} else if oldPWID != nil {
|
|
||||||
newPWID = oldPWID
|
|
||||||
clearPW = true
|
|
||||||
}
|
|
||||||
|
|
||||||
deltaQty := prep.receivedQty - item.TotalQty
|
deltaQty := prep.receivedQty - item.TotalQty
|
||||||
switch {
|
switch {
|
||||||
@@ -854,7 +871,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
VehicleNumber: prep.payload.VehicleNumber,
|
VehicleNumber: prep.payload.VehicleNumber,
|
||||||
ReceivedQty: &qtyCopy,
|
ReceivedQty: &qtyCopy,
|
||||||
ProductWarehouseID: newPWID,
|
ProductWarehouseID: newPWID,
|
||||||
ClearProductWarehouse: clearPW,
|
ClearProductWarehouse: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID {
|
if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID {
|
||||||
@@ -969,6 +986,38 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *purchaseService) uploadTravelDocument(
|
||||||
|
ctx context.Context,
|
||||||
|
actorID uint,
|
||||||
|
itemID uint,
|
||||||
|
file *multipart.FileHeader,
|
||||||
|
) (string, error) {
|
||||||
|
if file == nil {
|
||||||
|
return "", errors.New("travel document file is required")
|
||||||
|
}
|
||||||
|
if s.DocumentSvc == nil {
|
||||||
|
return "", errors.New("document service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentFiles := []commonSvc.DocumentFile{{
|
||||||
|
File: file,
|
||||||
|
Type: string(utils.DocumentTypePurchaseTravel),
|
||||||
|
}}
|
||||||
|
results, err := s.DocumentSvc.UploadDocuments(ctx, commonSvc.DocumentUploadRequest{
|
||||||
|
DocumentableType: string(utils.DocumentableTypePurchaseItem),
|
||||||
|
DocumentableID: uint64(itemID),
|
||||||
|
CreatedBy: &actorID,
|
||||||
|
Files: documentFiles,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
return "", errors.New("upload result is empty")
|
||||||
|
}
|
||||||
|
return results[0].URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1048,6 +1097,10 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
return nil, utils.Internal("Failed to delete purchase items")
|
return nil, utils.Internal("Failed to delete purchase items")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil {
|
||||||
|
return nil, utils.Internal("Failed to delete purchase documents")
|
||||||
|
}
|
||||||
|
|
||||||
if len(itemsToDelete) > 0 {
|
if len(itemsToDelete) > 0 {
|
||||||
if err := s.notifyExpenseItemsDeleted(ctx, purchase.Id, itemsToDelete); err != nil {
|
if err := s.notifyExpenseItemsDeleted(ctx, purchase.Id, itemsToDelete); err != nil {
|
||||||
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", purchase.Id, err)
|
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", purchase.Id, err)
|
||||||
@@ -1107,6 +1160,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|||||||
return utils.Internal("Failed to delete purchase")
|
return utils.Internal("Failed to delete purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil {
|
||||||
|
return utils.Internal("Failed to delete purchase documents")
|
||||||
|
}
|
||||||
|
|
||||||
if len(itemsToDelete) > 0 {
|
if len(itemsToDelete) > 0 {
|
||||||
if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil {
|
if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil {
|
||||||
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err)
|
s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err)
|
||||||
@@ -1190,6 +1247,21 @@ func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchas
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *purchaseService) deletePurchaseItemDocuments(ctx context.Context, items []entity.PurchaseItem) error {
|
||||||
|
if s.DocumentSvc == nil || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.DocumentSvc.DeleteByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id), true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *purchaseService) buildStaffAdjustmentPayload(
|
func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
purchase *entity.Purchase,
|
purchase *entity.Purchase,
|
||||||
@@ -1462,5 +1534,5 @@ func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
|
|||||||
return utils.Internal("DB not available for project flock validation")
|
return utils.Internal("DB not available for project flock validation")
|
||||||
}
|
}
|
||||||
|
|
||||||
return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs)
|
return commonSvc.EnsureProjectFlockNotClosedByProjectFlockKandangID(ctx, db, pfkIDs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import "mime/multipart"
|
||||||
|
|
||||||
type PurchaseItemPayload struct {
|
type PurchaseItemPayload struct {
|
||||||
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
|
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
|
||||||
ProductID uint `json:"product_id" validate:"required,gt=0"`
|
ProductID uint `json:"product_id" validate:"required,gt=0"`
|
||||||
@@ -26,7 +28,7 @@ type StaffPurchaseApprovalItem struct {
|
|||||||
|
|
||||||
type ApproveStaffPurchaseRequest struct {
|
type ApproveStaffPurchaseRequest struct {
|
||||||
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
||||||
Items []StaffPurchaseApprovalItem `json:"items,omitempty" validate:"omitempty,min=1,dive"`
|
Items []StaffPurchaseApprovalItem `json:"items" validate:"omitempty,min=1,dive"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,21 +38,22 @@ type ApproveManagerPurchaseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ReceivePurchaseItemRequest struct {
|
type ReceivePurchaseItemRequest struct {
|
||||||
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"`
|
PurchaseItemID uint `form:"purchase_item_id" json:"purchase_item_id" validate:"required,gt=0"`
|
||||||
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
|
WarehouseID *uint `form:"warehouse_id" json:"warehouse_id" validate:"omitempty,gt=0"`
|
||||||
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
|
ReceivedDate string `form:"received_date" json:"received_date" validate:"required,datetime=2006-01-02"`
|
||||||
ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
|
ExpeditionVendorID *uint `form:"expedition_vendor_id" json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
|
TransportPerItem *float64 `form:"transport_per_item" json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
|
||||||
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
|
TravelNumber *string `form:"travel_number" json:"travel_number" validate:"omitempty,max=100"`
|
||||||
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
|
TravelDocumentPath *string `form:"travel_document_path" json:"travel_document_path" validate:"omitempty,max=1024"`
|
||||||
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
|
VehicleNumber *string `form:"vehicle_number" json:"vehicle_number" validate:"omitempty,max=100"`
|
||||||
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"`
|
ReceivedQty *float64 `form:"received_qty" json:"received_qty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceivePurchaseRequest struct {
|
type ReceivePurchaseRequest struct {
|
||||||
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
||||||
Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"`
|
Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"`
|
||||||
|
Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeletePurchaseItemsRequest struct {
|
type DeletePurchaseItemsRequest struct {
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
|
|||||||
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
|
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
|
||||||
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
|
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
|
||||||
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
|
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
|
||||||
route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang)
|
route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll),ctrl.GetHppPerKandang)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -411,10 +426,12 @@ const (
|
|||||||
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
|
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
|
||||||
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
|
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
|
||||||
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
|
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
|
||||||
|
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
|
||||||
|
|
||||||
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
|
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
|
||||||
DocumentableTypeExpense DocumentableType = "EXPENSE"
|
DocumentableTypeExpense DocumentableType = "EXPENSE"
|
||||||
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
|
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
|
||||||
|
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -5,31 +5,6 @@ import (
|
|||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.RecordingBW {
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]entity.RecordingBW, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
var totalWeight float64
|
|
||||||
if item.TotalWeight != nil {
|
|
||||||
totalWeight = *item.TotalWeight
|
|
||||||
}
|
|
||||||
if totalWeight <= 0 {
|
|
||||||
totalWeight = item.AvgWeight * item.Qty
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, entity.RecordingBW{
|
|
||||||
RecordingId: recordingID,
|
|
||||||
AvgWeight: item.AvgWeight,
|
|
||||||
Qty: item.Qty,
|
|
||||||
TotalWeight: totalWeight,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
|
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -86,20 +61,3 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToGrams(weight float64) float64 {
|
|
||||||
if weight <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if weight < 10 {
|
|
||||||
return weight * 1000
|
|
||||||
}
|
|
||||||
return weight
|
|
||||||
}
|
|
||||||
|
|
||||||
func GramsToKg(grams float64) float64 {
|
|
||||||
if grams <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return grams / 1000
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user