mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c729067ab5 | |||
| f079bee92a | |||
| f1f7edb9ab | |||
| a4840fc98a | |||
| c3f8ae5887 | |||
| e545047165 | |||
| 42aa6829c5 | |||
| dded9e807b | |||
| 3bd0602525 | |||
| b1996be24c | |||
| 4a08be1f55 | |||
| 9f840f2650 | |||
| 80109b77db | |||
| df504e3ff0 | |||
| c1a162b4d4 | |||
| 1101879039 | |||
| 8de33a0f24 | |||
| 1348483b1c | |||
| 2f8f84cb0d | |||
| cc5a58b6d1 | |||
| fe51f33ab4 | |||
| e0dd2799fc | |||
| 394eb0f363 | |||
| 47d497d6b0 | |||
| 3a1a2b436d | |||
| 9d285869f5 | |||
| 4e2724a702 | |||
| 953756c15c | |||
| 2749e44439 | |||
| b8c0b0c37d | |||
| acbf52a5e1 | |||
| 0fc560b91c | |||
| d35d0bbe6b | |||
| d9afd2913e | |||
| dbaee73134 | |||
| 709e304f7f | |||
| d994cfdce7 | |||
| d3bb00a06a | |||
| 5302713811 | |||
| f698ca070c | |||
| 6c42119f4d | |||
| bc03c469f2 | |||
| fd5f83ca58 | |||
| 299c8c7177 | |||
| 78359db880 | |||
| 91fd8a253b | |||
| d91ff7a4c2 | |||
| 3ecea6741f | |||
| b988f45a0b | |||
| 10799cc1ed | |||
| c9c581ef30 | |||
| 6ee795cf2a | |||
| 471fd1dbbf | |||
| 4e5caa8cba | |||
| 0285852c42 | |||
| 0396aa0255 | |||
| 756ba223ed | |||
| 0c776e8332 | |||
| 90125ffe1a | |||
| c36719cc1a | |||
| e4acd9a21e | |||
| 9a094b8bfe | |||
| 16ef73fce3 | |||
| ddda696454 | |||
| 635049163e | |||
| 49af2d6448 | |||
| 68703d8752 | |||
| f19a3cb76e | |||
| d1ba13de76 | |||
| 6523290aaf | |||
| a2066979c1 | |||
| 8e7e976946 | |||
| e30ef5ef10 | |||
| bb76d27f25 | |||
| dbb13da7c4 | |||
| ac8536a4a1 | |||
| 96c2917834 | |||
| c3302397cc | |||
| c7ae836cf0 | |||
| 20f8a45823 | |||
| 67ddd8e667 | |||
| ebf0f8c5ab | |||
| 7dc5c9e9a5 | |||
| 306cf11fee | |||
| 9ee3b7582c | |||
| 8dfb224614 | |||
| 411d6fe6a9 | |||
| db4e8232b9 | |||
| 644896edfa | |||
| d945fcd19c | |||
| 812db3f79e | |||
| 10f42ed9c4 | |||
| a0d2c1c7dd | |||
| 56811f7c5b | |||
| 647bfbb667 | |||
| ec6da57510 | |||
| cdfa77566c | |||
| 1c875a916b | |||
| 85dc0ecd13 | |||
| c9633d1308 | |||
| b156e06cee | |||
| cd14de4dd2 | |||
| 54487b0fcf | |||
| a9037991ef | |||
| 12e5706318 | |||
| 3e575d96a7 | |||
| 98a34a1640 | |||
| c643e66282 | |||
| 9c3d0a44a6 | |||
| e935843cba | |||
| e33b23a2aa | |||
| c55fdb75a7 | |||
| 3a27917afc | |||
| c0132e5880 | |||
| 3d13cd966a | |||
| b41bb79125 | |||
| a2b8ebe665 | |||
| 2d8f20b70e | |||
| 824eb5905f | |||
| 817b6f82d0 | |||
| cbd3047a17 | |||
| ff4b4afcca | |||
| 240cd72204 | |||
| eae69a08fc | |||
| 17be6abc49 | |||
| ef117e66d1 | |||
| 4dfb988994 | |||
| dc726c49cf | |||
| a82df468d2 | |||
| 1af8f0a726 | |||
| 63068b8c3e | |||
| 5461c8b0ce | |||
| 5dc5f4c589 | |||
| ab9c7c216a | |||
| faa0861451 | |||
| 2eade07f0a | |||
| dbb9db960f | |||
| fa6d82b79a | |||
| 207382b3b0 | |||
| e551995c66 | |||
| cb076d92ac | |||
| f5c80fa560 | |||
| 14a4d9e944 | |||
| 84da0c27e0 | |||
| 047162699e | |||
| c95f90f0b9 | |||
| 9e0b4be4dd | |||
| f2df7f4847 | |||
| d675b1e826 | |||
| e52a02b1c0 | |||
| 096a446450 | |||
| 1b23861656 | |||
| a7069a2e50 | |||
| 3bfc401206 | |||
| 21d22c20a3 | |||
| d9a1372077 | |||
| 40f192660d | |||
| afe4b2ffe3 | |||
| eef254021c | |||
| cd739f41b9 | |||
| 8f77031e02 | |||
| 062a7937e2 | |||
| 4094d38d7b | |||
| d5bc6838c8 | |||
| efaeb89ca1 | |||
| a0a143b8ac | |||
| cbb3368141 | |||
| fc49cef781 | |||
| c79e35c217 | |||
| b8425c0f58 | |||
| 0de2021308 | |||
| c062d838e0 | |||
| 2dd3e3e271 | |||
| 2effa08648 | |||
| e6094528b5 |
@@ -3,7 +3,7 @@ root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
||||
bin = "tmp/main"
|
||||
full_bin = "APP_ENV=dev ./tmp/main"
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
|
||||
@@ -19,6 +19,7 @@ require (
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/xuri/excelize/v2 v2.9.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.11
|
||||
@@ -71,9 +72,12 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // 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/philhofer/fwd v1.1.2 // 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/rogpeppe/go-internal v1.11.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/cast v1.6.0 // 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/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.55.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/multierr v1.9.0 // 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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
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/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.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/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
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/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
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/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=
|
||||
@@ -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/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/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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package capabilities
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
permission "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
)
|
||||
|
||||
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||
// to toggle features. Only permissions recognized by the application are exposed.
|
||||
func FromPermissions(perms []string) map[string]bool {
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make(map[string]bool)
|
||||
for _, perm := range perms {
|
||||
if key, ok := normalizeAndAllow(perm); ok {
|
||||
out[key] = true
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeAndAllow(perm string) (string, bool) {
|
||||
perm = strings.ToLower(strings.TrimSpace(perm))
|
||||
if perm == "" {
|
||||
return "", false
|
||||
}
|
||||
if _, ok := allowed[perm]; !ok {
|
||||
return "", false
|
||||
}
|
||||
return perm, true
|
||||
}
|
||||
|
||||
var allowed = map[string]struct{}{
|
||||
permission.PermissionRecordingRead: {},
|
||||
permission.PermissionRecordingCreate: {},
|
||||
permission.PermissionRecordingUpdate: {},
|
||||
permission.PermissionRecordingDelete: {},
|
||||
}
|
||||
@@ -84,8 +84,9 @@ func (r *approvalRepositoryImpl) LatestByTargets(
|
||||
result := make(map[uint]entity.Approval, len(approvableIDs))
|
||||
|
||||
q := r.DB().WithContext(ctx).
|
||||
Select("DISTINCT ON (approvable_id) *").
|
||||
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
|
||||
Order("action_at DESC")
|
||||
Order("approvable_id, action_at DESC")
|
||||
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
|
||||
@@ -187,10 +187,11 @@ func (r *BaseRepositoryImpl[T]) PatchOne(
|
||||
updates map[string]any,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
q = q.Model(new(T)).Where("id = ?", id)
|
||||
|
||||
result := q.Updates(updates)
|
||||
if result.Error != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -9,45 +10,59 @@ import (
|
||||
|
||||
// Exists reports whether a record with the given ID exists for type T.
|
||||
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
var marker int
|
||||
err := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Select("1").
|
||||
Where("id = ?", id).
|
||||
Count(&count).Error; err != nil {
|
||||
Limit(1).
|
||||
Take(&marker).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
q := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Select("1").
|
||||
Where("name = ?", name).
|
||||
Where("deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
var marker int
|
||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||
if field == "" {
|
||||
return false, fmt.Errorf("field is required")
|
||||
}
|
||||
var count int64
|
||||
q := db.WithContext(ctx).
|
||||
Model(new(T)).
|
||||
Select("1").
|
||||
Where(fmt.Sprintf("%s = ?", field), value).
|
||||
Where("deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
var marker int
|
||||
if err := q.Limit(1).Take(&marker).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||
@@ -29,6 +31,7 @@ type DocumentService interface {
|
||||
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
|
||||
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
|
||||
PublicURL(document entity.Document) string
|
||||
PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type DocumentUploadRequest struct {
|
||||
@@ -293,6 +296,66 @@ func (s *documentService) PublicURL(document entity.Document) string {
|
||||
return s.storage.URL(document.Path)
|
||||
}
|
||||
|
||||
func (s *documentService) PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error) {
|
||||
if s.storage == nil {
|
||||
return "", errors.New("document storage not configured")
|
||||
}
|
||||
if strings.TrimSpace(document.Path) == "" {
|
||||
return "", errors.New("document path is required")
|
||||
}
|
||||
return s.storage.PresignURL(ctx, document.Path, expires)
|
||||
}
|
||||
|
||||
// ResolveDocumentURL normalizes a stored path or URL into a presigned URL.
|
||||
func ResolveDocumentURL(
|
||||
ctx context.Context,
|
||||
svc DocumentService,
|
||||
rawPath string,
|
||||
expires time.Duration,
|
||||
) (string, error) {
|
||||
if svc == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
rawPath = strings.TrimSpace(rawPath)
|
||||
if rawPath == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
key := rawPath
|
||||
lower := strings.ToLower(rawPath)
|
||||
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
||||
key = extractS3KeyFromURL(rawPath)
|
||||
if key == "" {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return svc.PresignURL(ctx, entity.Document{Path: key}, expires)
|
||||
}
|
||||
|
||||
func extractS3KeyFromURL(raw string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
path := strings.TrimPrefix(parsed.Path, "/")
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := strings.ToLower(strings.TrimSpace(parsed.Host))
|
||||
if strings.HasPrefix(host, "s3.") || strings.HasPrefix(host, "s3-") {
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
||||
normalizedExt := strings.TrimSpace(ext)
|
||||
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
@@ -17,6 +18,7 @@ type DocumentStorage interface {
|
||||
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
|
||||
Delete(ctx context.Context, key string) error
|
||||
URL(key string) string
|
||||
PresignURL(ctx context.Context, key string, expires time.Duration) (string, error)
|
||||
}
|
||||
|
||||
type DocumentStorageUploadResult struct {
|
||||
@@ -36,9 +38,10 @@ type S3DocumentStorageConfig struct {
|
||||
}
|
||||
|
||||
type s3DocumentStorage struct {
|
||||
client *s3.Client
|
||||
bucket string
|
||||
base string
|
||||
client *s3.Client
|
||||
presignClient *s3.PresignClient
|
||||
bucket string
|
||||
base string
|
||||
}
|
||||
|
||||
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
|
||||
@@ -86,6 +89,7 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
|
||||
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = cfg.ForcePathStyle
|
||||
})
|
||||
presignClient := s3.NewPresignClient(client)
|
||||
|
||||
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
|
||||
if baseURL == "" {
|
||||
@@ -97,9 +101,10 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
|
||||
}
|
||||
|
||||
return &s3DocumentStorage{
|
||||
client: client,
|
||||
bucket: bucket,
|
||||
base: baseURL,
|
||||
client: client,
|
||||
presignClient: presignClient,
|
||||
bucket: bucket,
|
||||
base: baseURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -158,3 +163,23 @@ func (s *s3DocumentStorage) URL(key string) string {
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", s.base, key)
|
||||
}
|
||||
|
||||
func (s *s3DocumentStorage) PresignURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
||||
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
|
||||
if key == "" {
|
||||
return "", errors.New("storage key is required")
|
||||
}
|
||||
if expires <= 0 {
|
||||
expires = 15 * time.Minute
|
||||
}
|
||||
|
||||
out, err := s.presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
}, s3.WithPresignExpires(expires))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out.URL, nil
|
||||
}
|
||||
|
||||
@@ -192,7 +192,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
||||
if req.Quantity < 0 {
|
||||
return nil, errors.New("quantity must be zero or greater")
|
||||
}
|
||||
|
||||
cfg, ok := fifo.Usable(req.UsableKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||
@@ -220,7 +219,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
||||
currentPending := ctxRow.PendingQty
|
||||
currentTotal := currentUsage + currentPending
|
||||
delta := req.Quantity - currentTotal
|
||||
|
||||
var (
|
||||
usageDelta float64
|
||||
pendingDelta float64
|
||||
@@ -285,7 +283,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
||||
result.ReleasedQuantity = releasedAmount
|
||||
result.UsageQuantity = currentUsage + usageDelta
|
||||
result.PendingQuantity = currentPending + pendingDelta
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -299,7 +296,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||
return errors.New("usable key and id are required")
|
||||
}
|
||||
|
||||
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||
cfg, ok := fifo.Usable(req.UsableKey)
|
||||
if !ok {
|
||||
@@ -310,7 +306,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var usageDelta, pendingDelta float64
|
||||
if ctxRow.UsageQty > 0 {
|
||||
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
||||
@@ -505,12 +500,25 @@ func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWa
|
||||
|
||||
var lots []stockLot
|
||||
for key, cfg := range configs {
|
||||
selectStmt := fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
cfg.Columns.CreatedAt,
|
||||
)
|
||||
|
||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||
|
||||
var selectStmt string
|
||||
if usesNumericTime {
|
||||
|
||||
selectStmt = fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
)
|
||||
} else {
|
||||
selectStmt = fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
cfg.Columns.CreatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint
|
||||
@@ -702,7 +710,7 @@ func (s *fifoService) releaseUsagePortion(
|
||||
}
|
||||
} else {
|
||||
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||
"quantity": allocation.Qty - releaseAmt,
|
||||
"qty": allocation.Qty - releaseAmt,
|
||||
}, func(db *gorm.DB) *gorm.DB {
|
||||
return s.txOrDB(tx, db)
|
||||
}); err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id
|
||||
|
||||
-- Relasi ke product_warehouses
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Relasi ke users
|
||||
ALTER TABLE project_chickins
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
DROP INDEX IF EXISTS idx_payments_bank_id;
|
||||
DROP INDEX IF EXISTS payments_party_polymorphic;
|
||||
DROP TABLE IF EXISTS payments;
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
payment_code VARCHAR(50) NOT NULL,
|
||||
reference_number VARCHAR(100) NULL,
|
||||
transaction_type VARCHAR(50),
|
||||
party_type VARCHAR(50) NOT NULL,
|
||||
party_id BIGINT NOT NULL,
|
||||
payment_date TIMESTAMPTZ NOT NULL,
|
||||
payment_method VARCHAR(20) NOT NULL,
|
||||
bank_id BIGINT NULL REFERENCES banks(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
direction VARCHAR(5) NOT NULL,
|
||||
nominal NUMERIC(15, 3) NOT NULL,
|
||||
notes TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX payments_party_polymorphic ON payments (party_type, party_id);
|
||||
CREATE INDEX idx_payments_bank_id ON payments (bank_id);
|
||||
@@ -0,0 +1,18 @@
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
trigger_name text;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'deleted_at'
|
||||
AND table_schema = 'public'
|
||||
GROUP BY table_schema, table_name
|
||||
LOOP
|
||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
DROP FUNCTION IF EXISTS soft_delete_handle_fk();
|
||||
@@ -0,0 +1,126 @@
|
||||
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
fk record;
|
||||
child_column text;
|
||||
parent_column text;
|
||||
parent_value text;
|
||||
child_has_deleted_at boolean;
|
||||
ref_exists boolean;
|
||||
sql text;
|
||||
BEGIN
|
||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||
FOR fk IN
|
||||
SELECT conrelid::regclass AS child_table,
|
||||
conkey AS child_cols,
|
||||
confkey AS parent_cols,
|
||||
confdeltype
|
||||
FROM pg_constraint
|
||||
WHERE contype = 'f'
|
||||
AND confrelid = TG_RELID
|
||||
LOOP
|
||||
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
||||
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
||||
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
SELECT attname INTO child_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attnum = fk.child_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attname INTO parent_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = TG_RELID
|
||||
AND attnum = fk.parent_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
EXECUTE format('SELECT ($1).%I', parent_column)
|
||||
INTO parent_value
|
||||
USING OLD;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attname = 'deleted_at'
|
||||
AND NOT attisdropped
|
||||
) INTO child_has_deleted_at;
|
||||
|
||||
IF fk.confdeltype IN ('r', 'a') THEN
|
||||
sql := format(
|
||||
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql INTO ref_exists USING parent_value;
|
||||
IF ref_exists THEN
|
||||
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
||||
TG_TABLE_NAME, fk.child_table;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'n' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSIF fk.confdeltype = 'c' THEN
|
||||
IF child_has_deleted_at THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
|
||||
fk.child_table,
|
||||
child_column
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSE
|
||||
sql := format(
|
||||
'DELETE FROM %s WHERE %I = $1',
|
||||
fk.child_table,
|
||||
child_column
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'd' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
trigger_name text;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'deleted_at'
|
||||
AND table_schema = 'public'
|
||||
GROUP BY table_schema, table_name
|
||||
LOOP
|
||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
||||
trigger_name,
|
||||
r.table_schema,
|
||||
r.table_name
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -0,0 +1 @@
|
||||
DROP SEQUENCE IF EXISTS payments_code_seq;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE SEQUENCE IF NOT EXISTS payments_code_seq START WITH 1 INCREMENT BY 1;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Rollback: restore document columns to expenses table
|
||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS document_path JSON;
|
||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS realization_document_path JSON;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Delete document columns from expenses table since we now use Document service with polymorphic relations
|
||||
ALTER TABLE expenses DROP COLUMN IF EXISTS document_path;
|
||||
ALTER TABLE expenses DROP COLUMN IF EXISTS realization_document_path;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-- ============================================
|
||||
-- Rollback: Remove FIFO fields and restore qty column
|
||||
-- ============================================
|
||||
|
||||
-- STEP 1: Drop indexes
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
|
||||
|
||||
-- STEP 2: Drop constraints
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
|
||||
|
||||
-- STEP 3: Restore qty column from usage_qty data
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||
|
||||
-- Migrate data back from usage_qty to qty
|
||||
UPDATE marketing_delivery_products
|
||||
SET qty = usage_qty
|
||||
WHERE qty = 0;
|
||||
|
||||
-- STEP 4: Drop FIFO columns
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS usage_qty,
|
||||
DROP COLUMN IF EXISTS pending_qty,
|
||||
DROP COLUMN IF EXISTS created_at;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
-- ============================================
|
||||
-- Add FIFO fields to marketing_delivery_products
|
||||
-- This migration adds fields needed for FIFO stock management
|
||||
-- and removes the old qty field in favor of FIFO-based allocation
|
||||
-- ============================================
|
||||
|
||||
-- STEP 0: Drop orphan indexes from previous migration
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
|
||||
|
||||
-- STEP 1: Add created_at column (required for FIFO ordering)
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
-- STEP 2: Add FIFO tracking fields
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
|
||||
|
||||
-- STEP 3: Migrate data from old qty to usage_qty for existing records
|
||||
-- This preserves existing quantity data as allocated quantity
|
||||
UPDATE marketing_delivery_products
|
||||
SET
|
||||
usage_qty = COALESCE(qty, 0),
|
||||
pending_qty = 0
|
||||
WHERE usage_qty = 0;
|
||||
|
||||
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS qty;
|
||||
|
||||
-- STEP 5: Make FIFO fields NOT NULL
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ALTER COLUMN usage_qty SET NOT NULL,
|
||||
ALTER COLUMN pending_qty SET NOT NULL,
|
||||
ALTER COLUMN created_at SET NOT NULL;
|
||||
|
||||
-- STEP 6: Add constraints to ensure non-negative values
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
|
||||
usage_qty >= 0 AND
|
||||
pending_qty >= 0
|
||||
);
|
||||
|
||||
-- STEP 7: Create indexes for FIFO operations
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
|
||||
ON marketing_delivery_products(created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
|
||||
ON marketing_delivery_products(usage_qty)
|
||||
WHERE usage_qty > 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
|
||||
ON marketing_delivery_products(pending_qty)
|
||||
WHERE pending_qty > 0;
|
||||
|
||||
-- Composite index for FIFO lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
|
||||
ON marketing_delivery_products(marketing_product_id, created_at DESC);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Remove foreign key constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
|
||||
|
||||
-- Drop product_warehouse_id column
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- Add product_warehouse_id column to marketing_delivery_products
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Fill product_warehouse_id from marketing_products
|
||||
UPDATE marketing_delivery_products mdp
|
||||
SET product_warehouse_id = mp.product_warehouse_id
|
||||
FROM marketing_products mp
|
||||
WHERE mdp.marketing_product_id = mp.id
|
||||
AND mdp.product_warehouse_id = 0;
|
||||
|
||||
-- Set NOT NULL constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ALTER COLUMN product_warehouse_id SET NOT NULL;
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_standard_growth_details_standard_week;
|
||||
DROP INDEX IF EXISTS idx_production_standard_details_standard_week;
|
||||
DROP INDEX IF EXISTS idx_production_standards_project_category;
|
||||
DROP INDEX IF EXISTS idx_production_standards_deleted_at;
|
||||
|
||||
-- Drop tables (in reverse order due to foreign keys)
|
||||
DROP TABLE IF EXISTS standard_growth_details;
|
||||
DROP TABLE IF EXISTS production_standard_details;
|
||||
DROP TABLE IF EXISTS production_standards;
|
||||
@@ -0,0 +1,96 @@
|
||||
-- Create production_standards table
|
||||
CREATE TABLE IF NOT EXISTS production_standards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
project_category VARCHAR(20) NOT NULL CHECK (project_category IN ('GROWING', 'LAYING')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT
|
||||
);
|
||||
|
||||
-- Create index for deleted_at (soft delete)
|
||||
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
|
||||
|
||||
-- Tambahkan Foreign Key ke users
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE production_standards
|
||||
ADD CONSTRAINT fk_production_standards_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Index
|
||||
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
|
||||
|
||||
-- Create production_standard_details table
|
||||
CREATE TABLE IF NOT EXISTS production_standard_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
production_standard_id BIGINT NOT NULL,
|
||||
week INT NOT NULL,
|
||||
target_hen_day_production NUMERIC(15, 3),
|
||||
target_hen_house_production NUMERIC(15, 3),
|
||||
target_egg_weight NUMERIC(15, 3),
|
||||
target_egg_mass NUMERIC(15, 3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Tambahkan Foreign Key ke production_standards
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
||||
ALTER TABLE production_standard_details
|
||||
ADD CONSTRAINT fk_production_standard_details_standard
|
||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create unique constraint for standard_id + week
|
||||
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
|
||||
ON production_standard_details(production_standard_id, week);
|
||||
|
||||
-- Create standard_growth_details table
|
||||
CREATE TABLE IF NOT EXISTS standard_growth_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
production_standard_id BIGINT NOT NULL,
|
||||
target_mean_bw NUMERIC(15, 3),
|
||||
max_depletion NUMERIC(15, 3),
|
||||
min_uniformity NUMERIC(15, 3) NOT NULL,
|
||||
week INT NOT NULL,
|
||||
feed_intake NUMERIC(15, 3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by BIGINT
|
||||
);
|
||||
|
||||
-- Tambahkan Foreign Key ke production_standards
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
||||
ALTER TABLE standard_growth_details
|
||||
ADD CONSTRAINT fk_standard_growth_details_standard
|
||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Tambahkan Foreign Key ke users
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE standard_growth_details
|
||||
ADD CONSTRAINT fk_standard_growth_details_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create unique constraint for standard_id + week
|
||||
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
|
||||
ON standard_growth_details(production_standard_id, week);
|
||||
|
||||
-- Index
|
||||
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
|
||||
|
||||
-- Create index for project_category
|
||||
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Rollback: Update expense and expense_nonstocks tables
|
||||
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_expenses_project_flock_id;
|
||||
DROP INDEX IF EXISTS idx_expenses_location_id;
|
||||
|
||||
-- Drop Foreign Key constraint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_expenses_location_id'
|
||||
) THEN
|
||||
ALTER TABLE expenses
|
||||
DROP CONSTRAINT fk_expenses_location_id;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Drop columns from expenses table
|
||||
ALTER TABLE expenses
|
||||
DROP COLUMN IF EXISTS project_flock_id;
|
||||
|
||||
ALTER TABLE expenses
|
||||
DROP COLUMN IF EXISTS location_id;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Migration: Update expense and expense_nonstocks tables
|
||||
|
||||
-- Add location_id column to expenses table
|
||||
ALTER TABLE expenses
|
||||
ADD COLUMN IF NOT EXISTS location_id BIGINT NOT NULL DEFAULT 1;
|
||||
|
||||
-- Add project_flock_id column to expenses table (JSON type)
|
||||
ALTER TABLE expenses
|
||||
ADD COLUMN IF NOT EXISTS project_flock_id JSON NULL;
|
||||
|
||||
-- Add Foreign Key constraint to locations table
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'locations') THEN
|
||||
ALTER TABLE expenses
|
||||
ADD CONSTRAINT fk_expenses_location_id
|
||||
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create index for location_id
|
||||
CREATE INDEX IF NOT EXISTS idx_expenses_location_id ON expenses (location_id);
|
||||
|
||||
-- Create index for project_flock_id
|
||||
CREATE INDEX IF NOT EXISTS idx_expenses_project_flock_id ON expenses ((project_flock_id::text));
|
||||
|
||||
-- Ensure kandang_id is nullable in expense_nonstocks table
|
||||
ALTER TABLE expense_nonstocks
|
||||
ALTER COLUMN kandang_id DROP NOT NULL;
|
||||
+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);
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
-- ===============================================================
|
||||
-- ROLLBACK: Remove FIFO fields from STOCK_TRANSFER_DETAILS
|
||||
-- ===============================================================
|
||||
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_stock_transfer_details_dest_pw;
|
||||
DROP INDEX IF EXISTS idx_stock_transfer_details_source_pw;
|
||||
|
||||
-- Drop foreign keys
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_stock_transfer_details_source_pw'
|
||||
) THEN
|
||||
EXECUTE 'ALTER TABLE stock_transfer_details
|
||||
DROP CONSTRAINT fk_stock_transfer_details_source_pw';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_stock_transfer_details_dest_pw'
|
||||
) THEN
|
||||
EXECUTE 'ALTER TABLE stock_transfer_details
|
||||
DROP CONSTRAINT fk_stock_transfer_details_dest_pw';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Drop FIFO columns
|
||||
ALTER TABLE stock_transfer_details
|
||||
DROP COLUMN IF EXISTS total_used,
|
||||
DROP COLUMN IF EXISTS total_qty,
|
||||
DROP COLUMN IF EXISTS pending_qty,
|
||||
DROP COLUMN IF EXISTS usage_qty,
|
||||
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||
DROP COLUMN IF EXISTS source_product_warehouse_id;
|
||||
|
||||
-- Restore original columns (in case rollback)
|
||||
ALTER TABLE stock_transfer_details
|
||||
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3),
|
||||
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3);
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
-- ===============================================================
|
||||
-- ADD FIFO FIELDS TO STOCK_TRANSFER_DETAILS
|
||||
-- Enable transfer module to work with FIFO stock system
|
||||
--
|
||||
-- Notes:
|
||||
-- - Field 'quantity' will be removed (replaced by usage_qty + pending_qty)
|
||||
-- - Fields 'before_quantity' & 'after_quantity' will be removed (unused legacy)
|
||||
-- - New FIFO fields track actual allocation instead of requested quantity
|
||||
-- ===============================================================
|
||||
|
||||
-- Add FIFO tracking fields
|
||||
ALTER TABLE stock_transfer_details
|
||||
ADD COLUMN IF NOT EXISTS source_product_warehouse_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS dest_product_warehouse_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS total_used NUMERIC(15, 3) DEFAULT 0;
|
||||
|
||||
-- Remove obsolete columns (quantity replaced by FIFO fields, legacy fields never used)
|
||||
ALTER TABLE stock_transfer_details
|
||||
DROP COLUMN IF EXISTS quantity,
|
||||
DROP COLUMN IF EXISTS before_quantity,
|
||||
DROP COLUMN IF EXISTS after_quantity;
|
||||
|
||||
-- Add foreign keys for product warehouse references
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
-- Source warehouse foreign key
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_stock_transfer_details_source_pw'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE stock_transfer_details
|
||||
ADD CONSTRAINT fk_stock_transfer_details_source_pw
|
||||
FOREIGN KEY (source_product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
-- Destination warehouse foreign key
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_stock_transfer_details_dest_pw'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE stock_transfer_details
|
||||
ADD CONSTRAINT fk_stock_transfer_details_dest_pw
|
||||
FOREIGN KEY (dest_product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add indexes for FIFO operations
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_source_pw
|
||||
ON stock_transfer_details (source_product_warehouse_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_dest_pw
|
||||
ON stock_transfer_details (dest_product_warehouse_id);
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON COLUMN stock_transfer_details.source_product_warehouse_id IS
|
||||
'Source product warehouse ID - referensi warehouse asal (FIFO usable)';
|
||||
|
||||
COMMENT ON COLUMN stock_transfer_details.dest_product_warehouse_id IS
|
||||
'Destination product warehouse ID - referensi warehouse tujuan (FIFO stockable)';
|
||||
|
||||
COMMENT ON COLUMN stock_transfer_details.usage_qty IS
|
||||
'Actual quantity successfully taken from source warehouse (FIFO usable tracking) - replaces quantity field';
|
||||
|
||||
COMMENT ON COLUMN stock_transfer_details.pending_qty IS
|
||||
'Quantity waiting for stock availability (FIFO usable tracking)';
|
||||
|
||||
COMMENT ON COLUMN stock_transfer_details.total_qty IS
|
||||
'Total lot quantity available at destination warehouse (FIFO stockable tracking)';
|
||||
|
||||
COMMENT ON COLUMN stock_transfer_details.total_used IS
|
||||
'Quantity already consumed from this lot at destination warehouse (FIFO stockable tracking)';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Rollback: Drop adjustment_stocks table
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS idx_adjustment_stocks_product_warehouse;
|
||||
DROP INDEX IF EXISTS idx_adjustment_stocks_stock_log;
|
||||
|
||||
ALTER TABLE adjustment_stocks
|
||||
DROP CONSTRAINT IF EXISTS fk_adjustment_stocks_product_warehouse;
|
||||
|
||||
ALTER TABLE adjustment_stocks
|
||||
DROP CONSTRAINT IF EXISTS fk_adjustment_stocks_stock_log;
|
||||
|
||||
DROP TABLE IF EXISTS adjustment_stocks;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Migration: Create adjustment_stocks table for FIFO tracking
|
||||
-- This table tracks FIFO allocation for stock adjustments (both increase and decrease)
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS adjustment_stocks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stock_log_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
|
||||
-- FIFO fields for Adjustment INCREASE (Stockable)
|
||||
-- Tracks stock added to warehouse via adjustment
|
||||
total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
total_used NUMERIC(15, 3) DEFAULT 0,
|
||||
|
||||
-- FIFO fields for Adjustment DECREASE (Usable)
|
||||
-- Tracks stock consumed from warehouse via adjustment
|
||||
usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
pending_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Foreign keys
|
||||
ALTER TABLE adjustment_stocks
|
||||
ADD CONSTRAINT fk_adjustment_stocks_stock_log
|
||||
FOREIGN KEY (stock_log_id) REFERENCES stock_logs(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE adjustment_stocks
|
||||
ADD CONSTRAINT fk_adjustment_stocks_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_adjustment_stocks_stock_log ON adjustment_stocks(stock_log_id);
|
||||
CREATE INDEX idx_adjustment_stocks_product_warehouse ON adjustment_stocks(product_warehouse_id);
|
||||
|
||||
COMMIT;
|
||||
+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;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
DROP INDEX IF EXISTS idx_project_flocks_production_standard_id;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP CONSTRAINT IF EXISTS fk_project_flocks_production_standard_id;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS production_standard_id;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
-- Add production_standard_id to project_flocks
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS production_standard_id BIGINT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
||||
ALTER TABLE project_flocks
|
||||
ADD CONSTRAINT fk_project_flocks_production_standard_id
|
||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_flocks_production_standard_id
|
||||
ON project_flocks (production_standard_id);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Remove standard_fcr column from production_standard_details table
|
||||
ALTER TABLE production_standard_details
|
||||
DROP COLUMN IF EXISTS standard_fcr;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Add standard_fcr column to production_standard_details table
|
||||
ALTER TABLE production_standard_details
|
||||
ADD COLUMN standard_fcr NUMERIC(15, 3);
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
-- Drop CASCADE constraint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_chickins_kandang'
|
||||
AND conrelid = 'project_chickins'::regclass
|
||||
) THEN
|
||||
ALTER TABLE project_chickins
|
||||
DROP CONSTRAINT fk_project_chickins_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Recreate foreign key constraint with RESTRICT (original behavior)
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_project_chickins_kandang
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Drop existing foreign key constraint with RESTRICT
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_chickins_kandang'
|
||||
AND conrelid = 'project_chickins'::regclass
|
||||
) THEN
|
||||
ALTER TABLE project_chickins
|
||||
DROP CONSTRAINT fk_project_chickins_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add new foreign key constraint with CASCADE delete
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_project_chickins_kandang
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,126 @@
|
||||
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
fk record;
|
||||
child_column text;
|
||||
parent_column text;
|
||||
parent_value text;
|
||||
child_has_deleted_at boolean;
|
||||
ref_exists boolean;
|
||||
sql text;
|
||||
BEGIN
|
||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||
FOR fk IN
|
||||
SELECT conrelid::regclass AS child_table,
|
||||
conkey AS child_cols,
|
||||
confkey AS parent_cols,
|
||||
confdeltype
|
||||
FROM pg_constraint
|
||||
WHERE contype = 'f'
|
||||
AND confrelid = TG_RELID
|
||||
LOOP
|
||||
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
||||
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
||||
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
SELECT attname INTO child_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attnum = fk.child_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attname INTO parent_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = TG_RELID
|
||||
AND attnum = fk.parent_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
EXECUTE format('SELECT ($1).%I', parent_column)
|
||||
INTO parent_value
|
||||
USING OLD;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attname = 'deleted_at'
|
||||
AND NOT attisdropped
|
||||
) INTO child_has_deleted_at;
|
||||
|
||||
IF fk.confdeltype IN ('r', 'a') THEN
|
||||
sql := format(
|
||||
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql INTO ref_exists USING parent_value;
|
||||
IF ref_exists THEN
|
||||
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
||||
TG_TABLE_NAME, fk.child_table;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'n' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSIF fk.confdeltype = 'c' THEN
|
||||
IF child_has_deleted_at THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
|
||||
fk.child_table,
|
||||
child_column
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSE
|
||||
sql := format(
|
||||
'DELETE FROM %s WHERE %I = $1',
|
||||
fk.child_table,
|
||||
child_column
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'd' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
trigger_name text;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'deleted_at'
|
||||
AND table_schema = 'public'
|
||||
GROUP BY table_schema, table_name
|
||||
LOOP
|
||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
||||
trigger_name,
|
||||
r.table_schema,
|
||||
r.table_name
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
@@ -0,0 +1,142 @@
|
||||
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
fk record;
|
||||
child_column text;
|
||||
parent_column text;
|
||||
parent_value text;
|
||||
child_has_deleted_at boolean;
|
||||
ref_exists boolean;
|
||||
sql text;
|
||||
child_type text;
|
||||
BEGIN
|
||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||
FOR fk IN
|
||||
SELECT conrelid::regclass AS child_table,
|
||||
conkey AS child_cols,
|
||||
confkey AS parent_cols,
|
||||
confdeltype
|
||||
FROM pg_constraint
|
||||
WHERE contype = 'f'
|
||||
AND confrelid = TG_RELID
|
||||
LOOP
|
||||
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
||||
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
||||
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
SELECT attname INTO child_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attnum = fk.child_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT attname INTO parent_column
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = TG_RELID
|
||||
AND attnum = fk.parent_cols[1]
|
||||
AND NOT attisdropped;
|
||||
|
||||
SELECT format_type(atttypid, atttypmod) INTO child_type
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attname = child_column
|
||||
AND NOT attisdropped;
|
||||
|
||||
IF child_type IS NULL THEN
|
||||
child_type := 'text';
|
||||
END IF;
|
||||
|
||||
EXECUTE format('SELECT ($1).%I', parent_column)
|
||||
INTO parent_value
|
||||
USING OLD;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_attribute
|
||||
WHERE attrelid = fk.child_table
|
||||
AND attname = 'deleted_at'
|
||||
AND NOT attisdropped
|
||||
) INTO child_has_deleted_at;
|
||||
|
||||
IF fk.confdeltype IN ('r', 'a') THEN
|
||||
sql := format(
|
||||
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1::%s %s)',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_type,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql INTO ref_exists USING parent_value;
|
||||
IF ref_exists THEN
|
||||
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
||||
TG_TABLE_NAME, fk.child_table;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'n' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = NULL WHERE %I = $1::%s %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
child_type,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSIF fk.confdeltype = 'c' THEN
|
||||
IF child_has_deleted_at THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1::%s AND deleted_at IS NULL',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_type
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
ELSE
|
||||
sql := format(
|
||||
'DELETE FROM %s WHERE %I = $1::%s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_type
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
ELSIF fk.confdeltype = 'd' THEN
|
||||
sql := format(
|
||||
'UPDATE %s SET %I = DEFAULT WHERE %I = $1::%s %s',
|
||||
fk.child_table,
|
||||
child_column,
|
||||
child_column,
|
||||
child_type,
|
||||
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||
);
|
||||
EXECUTE sql USING parent_value;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
r record;
|
||||
trigger_name text;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'deleted_at'
|
||||
AND table_schema = 'public'
|
||||
GROUP BY table_schema, table_name
|
||||
LOOP
|
||||
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
||||
trigger_name,
|
||||
r.table_schema,
|
||||
r.table_name
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
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;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_tables
|
||||
WHERE tablename = 'project_budgets'
|
||||
) THEN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_budgets_project_flock_id'
|
||||
) THEN
|
||||
ALTER TABLE project_budgets
|
||||
DROP CONSTRAINT fk_project_budgets_project_flock_id;
|
||||
END IF;
|
||||
|
||||
ALTER TABLE project_budgets
|
||||
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||
FOREIGN KEY (project_flock_id)
|
||||
REFERENCES project_flocks(id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_tables
|
||||
WHERE tablename = 'project_flock_kandang_uniformity'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'created_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
ADD COLUMN created_at TIMESTAMPTZ DEFAULT NOW();
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'updated_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'deleted_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
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 CASCADE ON UPDATE CASCADE;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_tables
|
||||
WHERE tablename = 'project_budgets'
|
||||
) THEN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_project_budgets_project_flock_id'
|
||||
) THEN
|
||||
ALTER TABLE project_budgets
|
||||
DROP CONSTRAINT fk_project_budgets_project_flock_id;
|
||||
END IF;
|
||||
|
||||
ALTER TABLE project_budgets
|
||||
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||
FOREIGN KEY (project_flock_id)
|
||||
REFERENCES project_flocks(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_trigger
|
||||
WHERE tgname = 'trg_soft_delete_fk_project_flock_kandang_uniformity'
|
||||
) THEN
|
||||
DROP TRIGGER trg_soft_delete_fk_project_flock_kandang_uniformity
|
||||
ON project_flock_kandang_uniformity;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'created_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
DROP COLUMN created_at;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'updated_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
DROP COLUMN updated_at;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flock_kandang_uniformity'
|
||||
AND column_name = 'deleted_at'
|
||||
) THEN
|
||||
ALTER TABLE project_flock_kandang_uniformity
|
||||
DROP COLUMN deleted_at;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,12 @@
|
||||
DROP TABLE IF EXISTS daily_checklist_tasks;
|
||||
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
|
||||
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
|
||||
DROP TABLE IF EXISTS daily_checklist_phases;
|
||||
DROP TABLE IF EXISTS daily_checklists;
|
||||
DROP TABLE IF EXISTS checklists;
|
||||
DROP TABLE IF EXISTS phase_activities;
|
||||
DROP TABLE IF EXISTS phases;
|
||||
DROP TABLE IF EXISTS employee_kandangs;
|
||||
DROP TABLE IF EXISTS employees;
|
||||
|
||||
DROP TYPE IF EXISTS category_code;
|
||||
@@ -0,0 +1,194 @@
|
||||
CREATE TYPE category_code AS ENUM (
|
||||
'pullet_open',
|
||||
'pullet_close',
|
||||
'produksi_open',
|
||||
'produksi_close'
|
||||
);
|
||||
|
||||
-- MASTER TABLES
|
||||
|
||||
CREATE TABLE employees (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar NOT NULL,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE employee_kandangs (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
employee_id bigint NOT NULL,
|
||||
kandang_id bigint NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_employee_kandangs_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_employee_kandangs_kandang
|
||||
FOREIGN KEY (kandang_id) REFERENCES kandangs(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT uq_employee_kandangs UNIQUE (employee_id, kandang_id)
|
||||
);
|
||||
|
||||
-- PHASE & CHECKLIST
|
||||
|
||||
CREATE TABLE phases (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar NOT NULL,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
category category_code NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE phase_activities (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
phase_id bigint NOT NULL,
|
||||
name varchar NOT NULL,
|
||||
description text,
|
||||
time_type text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_phase_activities_phase
|
||||
FOREIGN KEY (phase_id) REFERENCES phases(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE checklists (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar NOT NULL,
|
||||
description text,
|
||||
phase_id bigint,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
|
||||
CONSTRAINT fk_checklists_phase
|
||||
FOREIGN KEY (phase_id) REFERENCES phases(id)
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
|
||||
|
||||
-- DAILY CHECKLISTS
|
||||
CREATE TABLE daily_checklists (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
kandang_id bigint NOT NULL,
|
||||
checklist_id bigint NOT NULL,
|
||||
date date NOT NULL,
|
||||
name varchar,
|
||||
status varchar,
|
||||
category category_code NOT NULL,
|
||||
total_score integer,
|
||||
document_path varchar,
|
||||
reject_reason text,
|
||||
created_by bigint,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_daily_checklists_kandang
|
||||
FOREIGN KEY (kandang_id) REFERENCES kandangs(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_daily_checklists_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
|
||||
ON DELETE RESTRICT,
|
||||
|
||||
CONSTRAINT fk_daily_checklists_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
|
||||
|
||||
--RELASI CHECKLIST ⇄ PHASE
|
||||
|
||||
CREATE TABLE daily_checklist_phases (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
checklist_id bigint NOT NULL,
|
||||
phase_id bigint NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_dcp_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_dcp_phase
|
||||
FOREIGN KEY (phase_id) REFERENCES phases(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT uq_daily_checklist_phases UNIQUE (checklist_id, phase_id)
|
||||
);
|
||||
|
||||
|
||||
--ACTIVITY TASKS & ASSIGNMENT
|
||||
|
||||
|
||||
CREATE TABLE daily_checklist_activity_tasks (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
checklist_id bigint NOT NULL,
|
||||
phase_id bigint NOT NULL,
|
||||
phase_activity_id bigint NOT NULL,
|
||||
time_type text,
|
||||
notes text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_dcat_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_dcat_phase
|
||||
FOREIGN KEY (phase_id) REFERENCES phases(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_dcat_phase_activity
|
||||
FOREIGN KEY (phase_activity_id) REFERENCES phase_activities(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE daily_checklist_activity_task_assignments (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
task_id bigint NOT NULL,
|
||||
employee_id bigint NOT NULL,
|
||||
checked boolean NOT NULL DEFAULT false,
|
||||
note text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_assignment_task
|
||||
FOREIGN KEY (task_id) REFERENCES daily_checklist_activity_tasks(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_assignment_employee
|
||||
FOREIGN KEY (employee_id) REFERENCES employees(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
--DAILY CHECKLIST TASK RESULT
|
||||
CREATE TABLE daily_checklist_tasks (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
daily_checklist_id bigint NOT NULL,
|
||||
checklist_id bigint NOT NULL,
|
||||
checklist_item_id bigint,
|
||||
is_completed boolean NOT NULL DEFAULT false,
|
||||
score_value integer,
|
||||
notes text,
|
||||
photo_proof varchar,
|
||||
status varchar,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fk_dct_daily
|
||||
FOREIGN KEY (daily_checklist_id) REFERENCES daily_checklists(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_dct_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_dct_checklist_item
|
||||
FOREIGN KEY (checklist_item_id) REFERENCES phase_activities(id)
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE daily_checklists
|
||||
DROP CONSTRAINT IF EXISTS daily_checklists_date_kandang_category_key;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE daily_checklists
|
||||
ADD CONSTRAINT daily_checklists_date_kandang_category_key
|
||||
UNIQUE (date, kandang_id, category);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE daily_checklists
|
||||
ALTER COLUMN checklist_id SET NOT NULL;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE daily_checklists
|
||||
ALTER COLUMN checklist_id DROP NOT NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE daily_checklist_phases
|
||||
DROP CONSTRAINT IF EXISTS fk_dcp_daily_checklist,
|
||||
ADD CONSTRAINT fk_dcp_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE daily_checklist_phases
|
||||
DROP CONSTRAINT IF EXISTS fk_dcp_checklist,
|
||||
ADD CONSTRAINT fk_dcp_daily_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE daily_checklist_activity_tasks
|
||||
DROP CONSTRAINT IF EXISTS fk_dcat_daily_checklist,
|
||||
ADD CONSTRAINT fk_dcat_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE daily_checklist_activity_tasks
|
||||
DROP CONSTRAINT IF EXISTS fk_dcat_checklist,
|
||||
ADD CONSTRAINT fk_dcat_daily_checklist
|
||||
FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE daily_checklist_activity_task_assignments
|
||||
DROP CONSTRAINT IF EXISTS daily_checklist_activity_task_assignments_task_employee_key;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE daily_checklist_activity_task_assignments
|
||||
ADD CONSTRAINT daily_checklist_activity_task_assignments_task_employee_key
|
||||
UNIQUE (task_id, employee_id);
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE phase_activities
|
||||
DROP COLUMN IF EXISTS deleted_at;
|
||||
|
||||
ALTER TABLE phases
|
||||
DROP COLUMN IF EXISTS deleted_at;
|
||||
|
||||
ALTER TABLE employees
|
||||
DROP COLUMN IF EXISTS deleted_at;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE employees
|
||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE phases
|
||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE phase_activities
|
||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS config_checklists;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS config_checklists (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
percentage_threshold_bad INTEGER NOT NULL,
|
||||
percentage_threshold_enough INTEGER NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
+128
-696
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -25,66 +24,20 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
areas, err := seedAreas(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
locations, err := seedLocations(tx, adminID, areas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
productCategories, err := seedProductCategories(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := seedFlocks(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := seedFcr(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kandangs, err := seedKandangs(tx, adminID, locations, users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
suppliers, err := seedSuppliers(tx, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedCustomers(tx, adminID, users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedBanks(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedProductWarehouse(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedTransferStock(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("✅ Master data seeding completed")
|
||||
return nil
|
||||
})
|
||||
@@ -141,224 +94,6 @@ func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Priangan", "Banten"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
var area entity.Area
|
||||
err := tx.Where("name = ?", name).First(&area).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
area = entity.Area{Name: name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&area).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[name] = area.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Address string
|
||||
Area string
|
||||
}{
|
||||
{"Singaparna", "Tasik", "Priangan"},
|
||||
{"Cikaum", "Cikaum", "Banten"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
areaID, ok := areas[seed.Area]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("area %s not seeded", seed.Area)
|
||||
}
|
||||
|
||||
var loc entity.Location
|
||||
err := tx.Where("name = ?", seed.Name).First(&loc).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
loc = entity.Location{
|
||||
Name: seed.Name,
|
||||
Address: seed.Address,
|
||||
AreaId: areaID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&loc).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Name] = loc.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Flock Priangan", "Flock Banten"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
var flock entity.Flock
|
||||
err := tx.Where("name = ?", name).First(&flock).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
flock = entity.Flock{
|
||||
Name: name,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&flock).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
|
||||
"created_by": createdBy,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[name] = flock.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Status utils.KandangStatus
|
||||
Capacity float64
|
||||
Location string
|
||||
PicKey string
|
||||
}{
|
||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
locID, ok := locations[seed.Location]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("location %s not seeded", seed.Location)
|
||||
}
|
||||
picID, ok := users[seed.PicKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||
}
|
||||
|
||||
var kandang entity.Kandang
|
||||
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
kandang = entity.Kandang{
|
||||
Name: seed.Name,
|
||||
Status: string(seed.Status),
|
||||
LocationId: locID,
|
||||
PicId: picID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&kandang).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
updates := map[string]any{
|
||||
"location_id": locID,
|
||||
"pic_id": picID,
|
||||
"status": string(seed.Status),
|
||||
}
|
||||
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[seed.Name] = kandang.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Type string
|
||||
Area string
|
||||
Location *string
|
||||
Kandang *string
|
||||
}{
|
||||
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
|
||||
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
|
||||
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
|
||||
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
|
||||
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
|
||||
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
|
||||
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
|
||||
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
areaID, ok := areas[seed.Area]
|
||||
if !ok {
|
||||
return fmt.Errorf("area %s not seeded", seed.Area)
|
||||
}
|
||||
|
||||
var warehouse entity.Warehouse
|
||||
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
warehouse = entity.Warehouse{
|
||||
Name: seed.Name,
|
||||
Type: seed.Type,
|
||||
AreaId: areaID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if seed.Location != nil {
|
||||
locID, ok := locations[*seed.Location]
|
||||
if !ok {
|
||||
return fmt.Errorf("location %s not seeded", *seed.Location)
|
||||
}
|
||||
warehouse.LocationId = uintPtr(locID)
|
||||
}
|
||||
if seed.Kandang != nil {
|
||||
kandangID, ok := kandangs[*seed.Kandang]
|
||||
if !ok {
|
||||
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
|
||||
}
|
||||
warehouse.KandangId = uintPtr(kandangID)
|
||||
}
|
||||
|
||||
if warehouse.Id == 0 {
|
||||
if err := tx.Create(&warehouse).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
|
||||
"type": warehouse.Type,
|
||||
"area_id": warehouse.AreaId,
|
||||
"location_id": warehouse.LocationId,
|
||||
"kandang_id": warehouse.KandangId,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
@@ -440,113 +175,6 @@ func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
PicKey string
|
||||
Address string
|
||||
Phone string
|
||||
Email string
|
||||
}{
|
||||
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
|
||||
}
|
||||
|
||||
for idx, seed := range seeds {
|
||||
picID, ok := users[seed.PicKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||
}
|
||||
|
||||
var customer entity.Customer
|
||||
err := tx.Where("name = ?", seed.Name).First(&customer).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
customer = entity.Customer{
|
||||
Name: seed.Name,
|
||||
PicId: picID,
|
||||
Type: string(utils.CustomerSupplierTypeBisnis),
|
||||
Address: seed.Address,
|
||||
Phone: seed.Phone,
|
||||
Email: seed.Email,
|
||||
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&customer).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Standards []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}
|
||||
}{
|
||||
{
|
||||
Name: "FCR Layer",
|
||||
Standards: []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}{
|
||||
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
|
||||
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
var fcr entity.Fcr
|
||||
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
|
||||
if err := tx.Create(&fcr).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Name] = fcr.Id
|
||||
|
||||
for _, std := range seed.Standards {
|
||||
var standard entity.FcrStandard
|
||||
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
standard = entity.FcrStandard{
|
||||
FcrID: fcr.Id,
|
||||
Weight: std.Weight,
|
||||
FcrNumber: std.FcrNumber,
|
||||
Mortality: std.Mortality,
|
||||
}
|
||||
if err := tx.Create(&standard).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
|
||||
"fcr_number": std.FcrNumber,
|
||||
"mortality": std.Mortality,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
@@ -560,77 +188,88 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Expiry *int
|
||||
Suppliers []string
|
||||
Flags []utils.FlagType
|
||||
IsVisible bool
|
||||
}{
|
||||
{
|
||||
Name: "DOC Broiler",
|
||||
Brand: "MBU Broiler",
|
||||
Sku: "BRO0001",
|
||||
Name: "ISA Brown",
|
||||
Brand: "ISA Brown",
|
||||
Sku: "ISA0001",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 7500,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagDOC},
|
||||
Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer},
|
||||
IsVisible: true,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Pullet",
|
||||
Brand: "MBU Pullet",
|
||||
Sku: "PLT0001",
|
||||
Name: "Ayam Afkir",
|
||||
Brand: "-",
|
||||
Sku: "1",
|
||||
Uom: "Ekor",
|
||||
Category: "Pullet",
|
||||
Price: 15000,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPullet},
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagAyamAfkir},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Afkir",
|
||||
Brand: "-",
|
||||
Sku: "1",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Name: "Ayam Mati",
|
||||
Brand: "-",
|
||||
Sku: "2",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagAyamMati},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Mati",
|
||||
Brand: "-",
|
||||
Sku: "2",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Name: "Ayam Culling",
|
||||
Brand: "-",
|
||||
Sku: "3",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagAyamCulling},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Culling",
|
||||
Brand: "-",
|
||||
Sku: "3",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
Name: "Telur Utuh",
|
||||
Brand: "-",
|
||||
Sku: "4",
|
||||
Uom: "Gram",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurUtuh},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Telur Konsumsi Baik",
|
||||
Brand: "-",
|
||||
Sku: "4",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Name: "Telur Pecah",
|
||||
Brand: "-",
|
||||
Sku: "5",
|
||||
Uom: "Gram",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurPecah},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Telur Pecah",
|
||||
Brand: "-",
|
||||
Sku: "5",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Name: "Telur Putih",
|
||||
Brand: "-",
|
||||
Sku: "6",
|
||||
Uom: "Gram",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurPutih},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "281 SPECIAL STARTER",
|
||||
Brand: "281 STARTER",
|
||||
Sku: "281",
|
||||
Uom: "Kilogram",
|
||||
Category: "Bahan Baku",
|
||||
Price: 7850,
|
||||
Expiry: intPtr(60),
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
||||
Name: "Telur Retak",
|
||||
Brand: "-",
|
||||
Sku: "7",
|
||||
Uom: "Gram",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurRetak},
|
||||
IsVisible: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -709,78 +348,78 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Uom string
|
||||
Suppliers []string
|
||||
Flags []utils.FlagType
|
||||
}{
|
||||
{
|
||||
Name: "Expedisi DOC",
|
||||
Uom: "Ekor",
|
||||
Suppliers: []string{"Ekspedisi"},
|
||||
Flags: []utils.FlagType{utils.FlagEkspedisi},
|
||||
},
|
||||
{
|
||||
Name: "Solar",
|
||||
Uom: "Liter",
|
||||
Suppliers: []string{"BOP Vendor"},
|
||||
Flags: []utils.FlagType{},
|
||||
},
|
||||
}
|
||||
// func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
|
||||
// seeds := []struct {
|
||||
// Name string
|
||||
// Uom string
|
||||
// Suppliers []string
|
||||
// Flags []utils.FlagType
|
||||
// }{
|
||||
// {
|
||||
// Name: "LAJ",
|
||||
// Uom: "Unit",
|
||||
// Suppliers: []string{"Ekspedisi"},
|
||||
// Flags: []utils.FlagType{utils.FlagEkspedisi},
|
||||
// },
|
||||
// {
|
||||
// Name: "Solar",
|
||||
// Uom: "Liter",
|
||||
// Suppliers: []string{"BOP Vendor"},
|
||||
// Flags: []utils.FlagType{},
|
||||
// },
|
||||
// }
|
||||
|
||||
for _, seed := range seeds {
|
||||
uomID, ok := uoms[seed.Uom]
|
||||
if !ok {
|
||||
return fmt.Errorf("uom %s not seeded", seed.Uom)
|
||||
}
|
||||
// for _, seed := range seeds {
|
||||
// uomID, ok := uoms[seed.Uom]
|
||||
// if !ok {
|
||||
// return fmt.Errorf("uom %s not seeded", seed.Uom)
|
||||
// }
|
||||
|
||||
var nonstock entity.Nonstock
|
||||
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
nonstock = entity.Nonstock{
|
||||
Name: seed.Name,
|
||||
UomId: uomID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&nonstock).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
|
||||
"uom_id": uomID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// var nonstock entity.Nonstock
|
||||
// err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// nonstock = entity.Nonstock{
|
||||
// Name: seed.Name,
|
||||
// UomId: uomID,
|
||||
// CreatedBy: createdBy,
|
||||
// }
|
||||
// if err := tx.Create(&nonstock).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err != nil {
|
||||
// return err
|
||||
// } else {
|
||||
// if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
|
||||
// "uom_id": uomID,
|
||||
// }).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
for _, supplierName := range seed.Suppliers {
|
||||
supplierID, ok := suppliers[supplierName]
|
||||
if !ok {
|
||||
return fmt.Errorf("supplier %s not seeded", supplierName)
|
||||
}
|
||||
var existing entity.NonstockSupplier
|
||||
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
|
||||
if err := tx.Create(&link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// for _, supplierName := range seed.Suppliers {
|
||||
// supplierID, ok := suppliers[supplierName]
|
||||
// if !ok {
|
||||
// return fmt.Errorf("supplier %s not seeded", supplierName)
|
||||
// }
|
||||
// var existing entity.NonstockSupplier
|
||||
// err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
|
||||
// if err := tx.Create(&link).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// nanti saya isi
|
||||
|
||||
@@ -808,213 +447,6 @@ func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedBanks(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Alias string
|
||||
Owner *string
|
||||
AccountNumber string
|
||||
}{
|
||||
{
|
||||
Name: "Bank Central Asia",
|
||||
Alias: "BCA",
|
||||
AccountNumber: "1234567890",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
{
|
||||
Name: "Bank Rakyat Indonesia",
|
||||
Alias: "BRI",
|
||||
AccountNumber: "9876543210",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
{
|
||||
Name: "Bank Mandiri",
|
||||
Alias: "MAND",
|
||||
AccountNumber: "1122334455",
|
||||
Owner: ptr("PT MBU Group"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var bank entity.Bank
|
||||
err := tx.Where("name = ?", seed.Name).First(&bank).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
bank = entity.Bank{
|
||||
Name: seed.Name,
|
||||
Alias: seed.Alias,
|
||||
Owner: seed.Owner,
|
||||
AccountNumber: seed.AccountNumber,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
if err := tx.Create(&bank).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
// update data jika sudah ada
|
||||
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
|
||||
"alias": seed.Alias,
|
||||
"owner": seed.Owner,
|
||||
"account_number": seed.AccountNumber,
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
||||
seeds := []struct {
|
||||
ProductName string
|
||||
WarehouseName string
|
||||
Quantity float64
|
||||
}{
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var product entity.Product
|
||||
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var warehouse entity.Warehouse
|
||||
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
productWarehouse = entity.ProductWarehouse{
|
||||
ProductId: product.Id,
|
||||
WarehouseId: warehouse.Id,
|
||||
Quantity: seed.Quantity,
|
||||
// CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&productWarehouse).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&productWarehouse).Updates(map[string]any{
|
||||
"quantity": seed.Quantity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedTransferStock(tx *gorm.DB) error {
|
||||
|
||||
transfer := entity.StockTransfer{
|
||||
FromWarehouseId: 1,
|
||||
ToWarehouseId: 2,
|
||||
Reason: "Seed transfer stock",
|
||||
TransferDate: time.Now(),
|
||||
MovementNumber: "SEED-TRF-00001",
|
||||
CreatedBy: 1,
|
||||
}
|
||||
if err := tx.Create(&transfer).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
details := []entity.StockTransferDetail{
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
ProductId: 1,
|
||||
Quantity: 10,
|
||||
},
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
ProductId: 2,
|
||||
Quantity: 5,
|
||||
},
|
||||
}
|
||||
for i := range details {
|
||||
if err := tx.Create(&details[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
deliveries := []entity.StockTransferDelivery{
|
||||
{
|
||||
StockTransferId: transfer.Id,
|
||||
SupplierId: 1,
|
||||
VehiclePlate: "B 1234 XYZ",
|
||||
DriverName: "Driver Seed",
|
||||
DocumentPath: "seed.pdf",
|
||||
ShippingCostItem: 1000,
|
||||
ShippingCostTotal: 2000,
|
||||
},
|
||||
}
|
||||
for i := range deliveries {
|
||||
if err := tx.Create(&deliveries[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
}
|
||||
|
||||
deliveryItems := []entity.StockTransferDeliveryItem{
|
||||
{
|
||||
StockTransferDeliveryId: deliveries[0].Id,
|
||||
StockTransferDetailId: detailMap[1],
|
||||
Quantity: 50,
|
||||
},
|
||||
{
|
||||
StockTransferDeliveryId: deliveries[0].Id,
|
||||
StockTransferDetailId: detailMap[2],
|
||||
Quantity: 30,
|
||||
},
|
||||
}
|
||||
for i := range deliveryItems {
|
||||
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func intPtr(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func uintPtr(v uint) *uint {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// AdjustmentStock tracks FIFO allocation for stock adjustments
|
||||
// - For INCREASE adjustments (Stockable): Tracks stock added to warehouse
|
||||
// - For DECREASE adjustments (Usable): Tracks stock consumed from warehouse
|
||||
type AdjustmentStock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
|
||||
// === FIFO FIELDS FOR INCREASE ADJUSTMENT (Stockable) ===
|
||||
// Tracks stock added to warehouse via adjustment INCREASE
|
||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot quantity available
|
||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Quantity already used from this lot
|
||||
|
||||
// === FIFO FIELDS FOR DECREASE ADJUSTMENT (Usable) ===
|
||||
// Tracks stock consumed from warehouse via adjustment DECREASE
|
||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual quantity consumed
|
||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Pending quantity (waiting for stock)
|
||||
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
||||
|
||||
// Relations
|
||||
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ConfigChecklist struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Date time.Time `gorm:"type:date;not null"`
|
||||
PercentageThresholdBad int `gorm:"not null"`
|
||||
PercentageThresholdEnough int `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type DailyChecklist struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
KandangId uint `gorm:"not null"`
|
||||
ChecklistId *uint
|
||||
Date time.Time `gorm:"type:date;not null"`
|
||||
Name *string `gorm:"type:varchar(255)"`
|
||||
Status *string `gorm:"type:varchar(255)"`
|
||||
Category string `gorm:"type:category_code;not null"`
|
||||
TotalScore *int
|
||||
DocumentPath *string
|
||||
RejectReason *string
|
||||
CreatedBy *uint
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
Checklist *Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
|
||||
Creator *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Tasks []DailyChecklistTask `gorm:"foreignKey:DailyChecklistId;references:Id"`
|
||||
}
|
||||
|
||||
type DailyChecklistPhase struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ChecklistId uint `gorm:"not null"`
|
||||
PhaseId uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
|
||||
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
}
|
||||
|
||||
type DailyChecklistActivityTask struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ChecklistId uint `gorm:"not null"`
|
||||
PhaseId uint `gorm:"not null"`
|
||||
PhaseActivityId uint `gorm:"not null"`
|
||||
TimeType *string `gorm:"type:text"`
|
||||
Notes *string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
Checklist DailyChecklist `gorm:"foreignKey:ChecklistId;references:Id"`
|
||||
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
PhaseActivity PhaseActivity `gorm:"foreignKey:PhaseActivityId;references:Id"`
|
||||
Assignments []DailyChecklistActivityTaskAssignment `gorm:"foreignKey:TaskId;references:Id"`
|
||||
}
|
||||
|
||||
type DailyChecklistActivityTaskAssignment struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
TaskId uint `gorm:"not null"`
|
||||
EmployeeId uint `gorm:"not null"`
|
||||
Checked bool `gorm:"not null;default:false"`
|
||||
Note *string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
Task DailyChecklistActivityTask `gorm:"foreignKey:TaskId;references:Id"`
|
||||
Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"`
|
||||
}
|
||||
|
||||
type DailyChecklistTask struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
DailyChecklistId uint `gorm:"not null"`
|
||||
ChecklistId uint `gorm:"not null"`
|
||||
ChecklistItemId *uint
|
||||
IsCompleted bool `gorm:"not null;default:false"`
|
||||
ScoreValue *int
|
||||
Notes *string `gorm:"type:text"`
|
||||
PhotoProof *string
|
||||
Status *string
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
DailyChecklist *DailyChecklist `gorm:"foreignKey:DailyChecklistId;references:Id"`
|
||||
Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
|
||||
ChecklistItem *PhaseActivity `gorm:"foreignKey:ChecklistItemId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Employee struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null"`
|
||||
IsActive bool `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"`
|
||||
}
|
||||
|
||||
type Employees = Employee
|
||||
|
||||
type EmployeeKandang struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
EmployeeId uint `gorm:"not null"`
|
||||
KandangId uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"`
|
||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -13,8 +12,8 @@ type Expense struct {
|
||||
SupplierId uint64 `gorm:""`
|
||||
Category string `gorm:"type:varchar(50);not null"`
|
||||
PoNumber string `gorm:"type:varchar(50)"`
|
||||
DocumentPath sql.NullString `gorm:"type:json"`
|
||||
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||
LocationId uint64 `gorm:"not null"`
|
||||
ProjectFlockId *string `gorm:"type:json"`
|
||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||
Notes string `gorm:"type:text;column:notes"`
|
||||
@@ -23,8 +22,11 @@ type Expense struct {
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||
Location *Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
||||
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Initial struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ReferenceNumber string `gorm:"type:varchar(100);not null"`
|
||||
TransactionType string `gorm:"type:varchar(50);not null"`
|
||||
InitialBalanceType string `gorm:"type:varchar(20);not null"`
|
||||
PartyType string `gorm:"type:varchar(50);not null;index:initials_party_polymorphic,priority:1"`
|
||||
PartyId uint `gorm:"not null;index:initials_party_polymorphic,priority:2"`
|
||||
BankId *uint `gorm:"index"`
|
||||
Direction string `gorm:"type:varchar(5);not null"`
|
||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Notes string `gorm:"type:text;not null"`
|
||||
CreatedBy uint `gorm:"index" json:"-"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Bank Bank `gorm:"foreignKey:BankId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
@@ -5,15 +5,20 @@ import (
|
||||
)
|
||||
|
||||
type MarketingDeliveryProduct struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
|
||||
// FIFO Fields
|
||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
|
||||
|
||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Payment struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
PaymentCode string `gorm:"type:varchar(50);not null"`
|
||||
ReferenceNumber *string `gorm:"type:varchar(100)"`
|
||||
TransactionType string `gorm:"type:varchar(50)"`
|
||||
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
|
||||
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
|
||||
PaymentDate time.Time `gorm:"not null"`
|
||||
PaymentMethod string `gorm:"type:varchar(20);not null"`
|
||||
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
|
||||
Direction string `gorm:"type:varchar(5);not null"`
|
||||
Nominal float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Notes string `gorm:"type:text;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
CreatedBy uint `gorm:"index" json:"-"`
|
||||
|
||||
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
|
||||
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Phases struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null"`
|
||||
IsActive bool `gorm:"not null;default:true"`
|
||||
Category string `gorm:"type:category_code;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
}
|
||||
|
||||
type PhaseActivity struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
PhaseId uint `gorm:"not null"`
|
||||
Name string `gorm:"not null"`
|
||||
Description *string `gorm:"type:text"`
|
||||
TimeType *string `gorm:"type:text"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
}
|
||||
|
||||
type Checklist struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null"`
|
||||
Description *string `gorm:"type:text"`
|
||||
PhaseId *uint
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Phase *Phases `gorm:"foreignKey:PhaseId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductionStandard struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
|
||||
ProjectCategory string `gorm:"type:varchar(20);not null"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
DeletedAt *time.Time `gorm:"type:timestamptz"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductionStandardDetail struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ProductionStandardId uint `gorm:"not null"`
|
||||
Week int `gorm:"not null"`
|
||||
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
|
||||
StandardFCR *float64 `gorm:"type:numeric(15,3)"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
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"`
|
||||
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"
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type ProjectFlock struct {
|
||||
AreaId uint `gorm:"not null"`
|
||||
Category string `gorm:"type:varchar(20);not null"`
|
||||
FcrId uint `gorm:"not null"`
|
||||
ProductionStandardId uint `gorm:"column:production_standard_id"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
@@ -20,6 +21,7 @@ type ProjectFlock struct {
|
||||
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||
|
||||
@@ -13,11 +13,14 @@ type Recording struct {
|
||||
Day *int `gorm:"column:day"`
|
||||
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
||||
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"`
|
||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||
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"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
@@ -25,10 +28,17 @@ type Recording struct {
|
||||
|
||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
|
||||
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,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type StandardGrowthDetail struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ProductionStandardId uint `gorm:"not null"`
|
||||
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
|
||||
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
|
||||
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Week int `gorm:"not null"`
|
||||
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -20,4 +20,5 @@ type StockTransfer struct {
|
||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
@@ -2,16 +2,6 @@ package entities
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
LogTypeAdjustment = "ADJUSTMENT"
|
||||
LogTypeTransfer = "TRANSFER"
|
||||
)
|
||||
|
||||
const (
|
||||
TransactionTypeIncrease = "INCREASE"
|
||||
TransactionTypeDecrease = "DECREASE"
|
||||
)
|
||||
|
||||
type StockLog struct {
|
||||
Id uint `gorm:"primaryKey;column:id"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||
|
||||
@@ -4,20 +4,21 @@ import "time"
|
||||
|
||||
// DETAIL EKSPEDISI
|
||||
type StockTransferDelivery struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
SupplierId uint64
|
||||
VehiclePlate string
|
||||
DriverName string
|
||||
DocumentNumber string
|
||||
DocumentPath string
|
||||
ShippingCostItem float64
|
||||
ShippingCostTotal float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
}
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
SupplierId uint64
|
||||
VehiclePlate string
|
||||
DriverName string
|
||||
DocumentNumber string
|
||||
DocumentPath string
|
||||
ShippingCostItem float64
|
||||
ShippingCostTotal float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
@@ -7,12 +7,28 @@ type StockTransferDetail struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
ProductId uint64
|
||||
Quantity float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Product *Product `gorm:"foreignKey:ProductId"`
|
||||
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
||||
|
||||
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
|
||||
// Tracking stock yang DIAMBIL dari source warehouse
|
||||
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
|
||||
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
|
||||
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
|
||||
|
||||
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) ===
|
||||
// Tracking stock yang DITAMBAHKAN ke destination warehouse
|
||||
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
|
||||
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
|
||||
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
|
||||
|
||||
// === METADATA ===
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
|
||||
// === RELATIONS ===
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Product *Product `gorm:"foreignKey:ProductId"`
|
||||
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
|
||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
|
||||
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Transaction 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -1,14 +1,234 @@
|
||||
package middleware
|
||||
|
||||
//project-flock
|
||||
// project-flock
|
||||
const (
|
||||
PermissionProjectFlockClosing = "lti:project-flock:closing"
|
||||
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
|
||||
P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail"
|
||||
P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list"
|
||||
P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail"
|
||||
|
||||
P_ProjectFlockGetAll = "lti.production.project_flocks.list"
|
||||
P_ProjectFlockCreate = "lti.production.project_flocks.create"
|
||||
P_ProjectFlockGetOne = "lti.production.project_flocks.detail"
|
||||
P_ProjectFlockUpdate = "lti.production.project_flocks.update"
|
||||
P_ProjectFlockDelete = "lti.production.project_flocks.delete"
|
||||
P_ProjectFlockApprove = "lti.production.project_flocks.approve"
|
||||
P_ProjectFlockLookup = "lti.production.project_flocks.lookup"
|
||||
P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period"
|
||||
P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit"
|
||||
)
|
||||
|
||||
//recording
|
||||
const (
|
||||
PermissionRecordingRead = "recording.index"
|
||||
PermissionRecordingCreate = "recording.create"
|
||||
PermissionRecordingUpdate = "recording.update"
|
||||
PermissionRecordingDelete = "recording.delete"
|
||||
)
|
||||
P_ExpenseGetAll = "lti.expense.list"
|
||||
P_ExpenseCreateOne = "lti.expense.create"
|
||||
P_ExpenseUpdateOne = "lti.expense.update"
|
||||
P_ExpenseGetOne = "lti.expense.detail"
|
||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
||||
P_ExpenseApprovalManager = "lti.expense.approve.manager"
|
||||
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
||||
P_ExpenseDocument = "lti.expense.document"
|
||||
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
|
||||
)
|
||||
const (
|
||||
P_AdjustmentGetAll = "lti.inventory.list"
|
||||
P_AdjustmentCreate = "lti.inventory.create"
|
||||
P_AdjustmentGetOne = "lti.inventory.detail"
|
||||
)
|
||||
const (
|
||||
P_ApprovalGetAll = "lti.approval.list"
|
||||
)
|
||||
const (
|
||||
P_ReportExpenseGetAll = "lti.repport.expense.list"
|
||||
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
|
||||
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
|
||||
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
|
||||
)
|
||||
|
||||
const (
|
||||
P_ProductStockGetAll = "lti.inventory.product_stock.list"
|
||||
P_ProductStockGetOne = "lti.inventory.product_stock.detail"
|
||||
P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list"
|
||||
P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail"
|
||||
)
|
||||
const (
|
||||
P_ClosingGetAll = "lti.closing.list"
|
||||
P_ClosingDetail = "lti.closing.detail"
|
||||
)
|
||||
|
||||
const (
|
||||
P_TransferGetAll = "lti.inventory.transfer.list"
|
||||
P_TransferGetOne = "lti.inventory.transfer.detail"
|
||||
P_TransferCreateOne = "lti.inventory.transfer.create"
|
||||
)
|
||||
|
||||
const (
|
||||
P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list"
|
||||
P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail"
|
||||
P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create"
|
||||
P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update"
|
||||
P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete"
|
||||
P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve"
|
||||
P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty"
|
||||
)
|
||||
|
||||
const (
|
||||
P_DeliveryGetAll = "lti.marketing.delivery_order.list"
|
||||
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
||||
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
||||
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
|
||||
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
||||
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
||||
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
||||
P_SalesOrderUpdateOne = "lti.marketing.sales_order.update"
|
||||
)
|
||||
|
||||
const (
|
||||
P_AreaGetAll = "lti.master.area.list"
|
||||
P_AreaGetOne = "lti.master.area.detail"
|
||||
P_AreaCreateOne = "lti.master.area.create"
|
||||
P_AreaUpdateOne = "lti.master.area.update"
|
||||
P_AreaDeleteOne = "lti.master.area.delete"
|
||||
|
||||
P_BanksGetAll = "lti.master.banks.list"
|
||||
P_BanksGetOne = "lti.master.banks.detail"
|
||||
P_BanksCreateOne = "lti.master.banks.create"
|
||||
P_BanksUpdateOne = "lti.master.banks.update"
|
||||
P_BanksDeleteOne = "lti.master.banks.delete"
|
||||
|
||||
P_CustomerGetAll = "lti.master.customer.list"
|
||||
P_CustomerGetOne = "lti.master.customer.detail"
|
||||
P_CustomerCreateOne = "lti.master.customer.create"
|
||||
P_CustomerUpdateOne = "lti.master.customer.update"
|
||||
P_CustomerDeleteOne = "lti.master.customer.delete"
|
||||
|
||||
P_FcrGetAll = "lti.master.fcr.list"
|
||||
P_FcrGetOne = "lti.master.fcr.detail"
|
||||
P_FcrCreateOne = "lti.master.fcr.create"
|
||||
P_FcrUpdateOne = "lti.master.fcr.update"
|
||||
P_FcrDeleteOne = "lti.master.fcr.delete"
|
||||
|
||||
P_FlocksGetAll = "lti.master.flocks.list"
|
||||
P_FlocksGetOne = "lti.master.flocks.detail"
|
||||
P_FlocksCreateOne = "lti.master.flocks.create"
|
||||
P_FlocksUpdateOne = "lti.master.flocks.update"
|
||||
P_FlocksDeleteOne = "lti.master.flocks.delete"
|
||||
|
||||
P_KandangsGetAll = "lti.master.kandangs.list"
|
||||
P_KandangsGetOne = "lti.master.kandangs.detail"
|
||||
P_KandangsCreateOne = "lti.master.kandangs.create"
|
||||
P_KandangsUpdateOne = "lti.master.kandangs.update"
|
||||
P_KandangsDeleteOne = "lti.master.kandangs.delete"
|
||||
|
||||
P_LocationsGetAll = "lti.master.locations.list"
|
||||
P_LocationsGetOne = "lti.master.locations.detail"
|
||||
P_LocationsCreateOne = "lti.master.locations.create"
|
||||
P_LocationsUpdateOne = "lti.master.locations.update"
|
||||
P_LocationsDeleteOne = "lti.master.locations.delete"
|
||||
|
||||
P_NonstocksGetAll = "lti.master.nonstocks.list"
|
||||
P_NonstocksGetOne = "lti.master.nonstocks.detail"
|
||||
P_NonstocksCreateOne = "lti.master.nonstocks.create"
|
||||
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
|
||||
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
|
||||
|
||||
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
|
||||
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
|
||||
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
|
||||
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
|
||||
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
|
||||
|
||||
P_ProductsGetAll = "lti.master.Products.list"
|
||||
P_ProductsGetOne = "lti.master.Products.detail"
|
||||
P_ProductsCreateOne = "lti.master.Products.create"
|
||||
P_ProductsUpdateOne = "lti.master.Products.update"
|
||||
P_ProductsDeleteOne = "lti.master.Products.delete"
|
||||
|
||||
P_SuppliersGetAll = "lti.master.suppliers.list"
|
||||
P_SuppliersGetOne = "lti.master.suppliers.detail"
|
||||
P_SuppliersCreateOne = "lti.master.suppliers.create"
|
||||
P_SuppliersUpdateOne = "lti.master.suppliers.update"
|
||||
P_SuppliersDeleteOne = "lti.master.suppliers.delete"
|
||||
|
||||
P_UomsGetAll = "lti.master.uoms.list"
|
||||
P_UomsGetOne = "lti.master.uoms.detail"
|
||||
P_UomsCreateOne = "lti.master.uoms.create"
|
||||
P_UomsUpdateOne = "lti.master.uoms.update"
|
||||
P_UomsDeleteOne = "lti.master.uoms.delete"
|
||||
|
||||
P_WarehousesGetAll = "lti.master.warehouses.list"
|
||||
P_WarehousesGetOne = "lti.master.warehouses.detail"
|
||||
P_WarehousesCreateOne = "lti.master.warehouses.create"
|
||||
P_WarehousesUpdateOne = "lti.master.warehouses.update"
|
||||
P_WarehousesDeleteOne = "lti.master.warehouses.delete"
|
||||
|
||||
P_Production_Standart_GetAll = "lti.master.production_standards.list"
|
||||
P_Production_Standart_CreateOne = "lti.master.production_standards.create"
|
||||
P_Production_Standart_GetOne = "lti.master.production_standards.detail"
|
||||
P_Production_Standart_UpdateOne = "lti.master.production_standards.update"
|
||||
P_Production_Standart_DeleteOne = "lti.master.production_standards.delete"
|
||||
)
|
||||
|
||||
// finance
|
||||
const (
|
||||
P_Finances_Initial_Balances_CreateOne = "lti.finance.initial_balances.create"
|
||||
P_Finances_Initial_Balances_GetOne = "lti.finance.initial_balances.detail"
|
||||
P_Finances_Initial_Balances_UpdateOne = "lti.finance.initial_balances.update"
|
||||
|
||||
P_Finances_Injections_CreateOne = "lti.finance.injections.create"
|
||||
P_Finances_Injections_GetOne = "lti.finance.injections.detail"
|
||||
P_Finances_Injections_UpdateOne = "lti.finance.injections.update"
|
||||
|
||||
P_Finances_Payments_CreateOne = "lti.finance.payments.create"
|
||||
P_Finances_Payments_UpdateOne = "lti.finance.payments.update"
|
||||
P_Finances_Payments_GetOne = "lti.finance.payments.detail"
|
||||
|
||||
P_Finances_Transaction_GetAll = "lti.finance.transactions.list"
|
||||
P_Finances_Transaction_GetOne = "lti.finance.transactions.detail"
|
||||
P_Finances_Transaction_DeleteOne = "lti.finance.transactions.delete"
|
||||
)
|
||||
const (
|
||||
P_ChickinsCreateOne = "lti.production.chickins.create"
|
||||
P_ChickinsGetOne = "lti.production.chickins.detail"
|
||||
P_ChickinsApproval = "lti.production.chickins.approve"
|
||||
)
|
||||
|
||||
// recording
|
||||
const (
|
||||
P_RecordingGetAll = "lti.production.recording.list"
|
||||
P_RecordingGetOne = "lti.production.recording.detail"
|
||||
P_RecordingCreateOne = "lti.production.recording.create"
|
||||
P_RecordingUpdateOne = "lti.production.recording.update"
|
||||
P_RecordingDeleteOne = "lti.production.recording.delete"
|
||||
P_RecordingNextDay = "lti.production.recording.next_day"
|
||||
P_RecordingApproval = "lti.production.recording.approve"
|
||||
)
|
||||
|
||||
const (
|
||||
P_PurchaseGetAll = "lti.Purchase.list"
|
||||
P_PurchaseGetOne = "lti.Purchase.detail"
|
||||
P_PurchaseCreateOne = "lti.Purchase.create"
|
||||
P_PurchaseUpdateOne = "lti.Purchase.update"
|
||||
P_PurchaseDeleteOne = "lti.Purchase.delete"
|
||||
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
|
||||
P_PurchaseReceive = "lti.Purchase.receive"
|
||||
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
|
||||
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
|
||||
)
|
||||
|
||||
const (
|
||||
P_Uniformities_GetAll = "lti.production.uniformity.list"
|
||||
P_Uniformities_GetOne = "lti.production.uniformity.detail"
|
||||
P_Uniformities_Verify = "lti.production.uniformity.verify"
|
||||
P_Uniformities_CreateOne = "lti.production.uniformity.create"
|
||||
P_Uniformities_UpdateOne = "lti.production.uniformity.update"
|
||||
P_Uniformities_DeleteOne = "lti.production.uniformity.delete"
|
||||
P_Uniformities_Approval = "lti.production.uniformity.approve"
|
||||
)
|
||||
|
||||
const (
|
||||
P_UserGetAll = "lti.users.list"
|
||||
P_UserGetOne = "lti.users.detail"
|
||||
)
|
||||
|
||||
@@ -15,5 +15,5 @@ func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalServic
|
||||
route := v1.Group("/approvals")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll))
|
||||
}
|
||||
|
||||
@@ -245,3 +245,109 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get closing keuangan successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
var projectFlockKandangID *uint
|
||||
if raw := c.Query("project_flock_kandang_id"); raw != "" {
|
||||
idInt, convErr := strconv.Atoi(raw)
|
||||
if convErr != nil || idInt <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
idUint := uint(idInt)
|
||||
projectFlockKandangID = &idUint
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get expedition HPP successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error {
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectFlockID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
kandangID := uint(pfkID)
|
||||
|
||||
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get expedition HPP successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Retrieved production data successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,18 +28,19 @@ type ClosingDetailDTO struct {
|
||||
}
|
||||
|
||||
type ClosingListItemDTO struct {
|
||||
Id uint `json:"id"`
|
||||
LocationID uint `json:"location_id"`
|
||||
LocationName string `json:"location_name"`
|
||||
ProjectCategory string `json:"project_category"`
|
||||
Period int `json:"period"`
|
||||
ClosingDate string `json:"closing_date"`
|
||||
ShedLabel string `json:"shed_label"`
|
||||
ShedCount int `json:"shed_count"`
|
||||
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
ProjectStatus string `json:"project_status"`
|
||||
Id uint `json:"id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
LocationID uint `json:"location_id"`
|
||||
LocationName string `json:"location_name"`
|
||||
ProjectCategory string `json:"project_category"`
|
||||
Period int `json:"period"`
|
||||
ClosingDate string `json:"closing_date"`
|
||||
ShedLabel string `json:"shed_label"`
|
||||
ShedCount int `json:"shed_count"`
|
||||
// SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||
// SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||
// SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
ProjectStatus string `json:"project_status"`
|
||||
}
|
||||
|
||||
type ClosingSummaryDTO struct {
|
||||
@@ -58,6 +59,52 @@ type ClosingSummaryDTO struct {
|
||||
StatusClosing string `json:"closing_status"`
|
||||
}
|
||||
|
||||
type ClosingPurchaseDTO struct {
|
||||
InitialPopulation int `json:"initial_population"`
|
||||
ClaimCulling int `json:"claim_culling"`
|
||||
FinalPopulation int `json:"final_population"`
|
||||
FeedIn float64 `json:"feed_in"`
|
||||
FeedUsed float64 `json:"feed_used"`
|
||||
FeedUsedPerHead float64 `json:"feed_used_per_head"`
|
||||
}
|
||||
|
||||
type ClosingSalesDTO struct {
|
||||
SalesPopulation int `json:"sales_population"`
|
||||
SalesWeight float64 `json:"sales_weight"`
|
||||
AverageWeight float64 `json:"average_weight"`
|
||||
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
|
||||
}
|
||||
|
||||
type ClosingEggSalesDTO struct {
|
||||
EggPieces int `json:"egg_pieces"`
|
||||
EggMassKg float64 `json:"egg_mass_kg"`
|
||||
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
|
||||
AverageSellingPrice float64 `json:"egg_average_selling_price"`
|
||||
}
|
||||
|
||||
type ClosingPerformanceDTO struct {
|
||||
Depletion float64 `json:"depletion"`
|
||||
Age float64 `json:"age_day"`
|
||||
MortalityStd float64 `json:"mortality_std"`
|
||||
MortalityAct float64 `json:"mortality_act"`
|
||||
DeffMortality float64 `json:"deff_mortality"`
|
||||
FcrStd float64 `json:"fcr_std"`
|
||||
FcrAct float64 `json:"fcr_act"`
|
||||
DeffFcr float64 `json:"deff_fcr"`
|
||||
Awg float64 `json:"awg"`
|
||||
}
|
||||
|
||||
type ClosingSalesGroupDTO struct {
|
||||
Chicken ClosingSalesDTO `json:"chicken"`
|
||||
Egg *ClosingEggSalesDTO `json:"egg,omitempty"`
|
||||
}
|
||||
|
||||
type ClosingProductionReportDTO struct {
|
||||
Purchase ClosingPurchaseDTO `json:"purchase"`
|
||||
Sales ClosingSalesGroupDTO `json:"sales"`
|
||||
Performance ClosingPerformanceDTO `json:"performance"`
|
||||
}
|
||||
|
||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||
history := project.KandangHistory
|
||||
|
||||
@@ -87,18 +134,19 @@ func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) Clo
|
||||
shedCount := len(project.KandangHistory)
|
||||
|
||||
return ClosingListItemDTO{
|
||||
Id: project.Id,
|
||||
LocationID: project.LocationId,
|
||||
LocationName: project.Location.Name,
|
||||
ProjectCategory: project.Category,
|
||||
Period: maxPeriod(project.KandangHistory),
|
||||
ClosingDate: "17-Nov-2025",
|
||||
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||
ShedCount: shedCount,
|
||||
SalesPaidAmount: 21993726,
|
||||
SalesRemainingAmount: 11075919,
|
||||
SalesPaymentStatus: "Lunas",
|
||||
ProjectStatus: projectStatus,
|
||||
Id: project.Id,
|
||||
ProjectName: project.FlockName,
|
||||
LocationID: project.LocationId,
|
||||
LocationName: project.Location.Name,
|
||||
ProjectCategory: project.Category,
|
||||
Period: maxPeriod(project.KandangHistory),
|
||||
ClosingDate: "17-Nov-2025",
|
||||
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||
ShedCount: shedCount,
|
||||
// SalesPaidAmount: 21993726,
|
||||
// SalesRemainingAmount: 11075919,
|
||||
// SalesPaymentStatus: "Lunas",
|
||||
ProjectStatus: projectStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +206,20 @@ func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
|
||||
ClosingListDTO: ToClosingListDTO(e),
|
||||
}
|
||||
}
|
||||
|
||||
func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
||||
for _, chickin := range projectFlockKandang.Chickins {
|
||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
||||
earliestChickinDate = chickin.ChickInDate
|
||||
}
|
||||
}
|
||||
|
||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
||||
ageInWeeks := ageInDays / 7
|
||||
return ageInWeeks
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package dto
|
||||
|
||||
// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor.
|
||||
type ExpeditionCostItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
ExpeditionVendorName string `json:"expedition_vendor_name"`
|
||||
HPPAmount float64 `json:"hpp_amount"`
|
||||
}
|
||||
|
||||
// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi.
|
||||
type ExpeditionHPPDTO struct {
|
||||
ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"`
|
||||
TotalHPPAmount float64 `json:"total_hpp_amount"`
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
// === CONSTANTS ===
|
||||
const (
|
||||
HPPGroupPengeluaran = "HPP dan Pengeluaran"
|
||||
HPPGroupBahanBaku = "HPP dan Bahan Baku"
|
||||
HPPLabelOverhead = "Pengeluaran Overhead"
|
||||
HPPLabelEkspedisi = "Beban Ekspedisi"
|
||||
HPPSummaryLabel = "HPP"
|
||||
|
||||
PLSalesTypeChicken = "Penjualan Ayam Besar"
|
||||
PLSalesTypeEgg = "Penjualan Telur"
|
||||
|
||||
PLItemTypeSapronak = "Pembelian Sapronak"
|
||||
PLItemTypeOverhead = "Pengeluaran Overhead"
|
||||
PLItemTypeEkspedisi = "Beban Ekspedisi"
|
||||
|
||||
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
|
||||
PLSummaryLabelSubTotal = "SUB TOTAL"
|
||||
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
|
||||
|
||||
PurchaseLabelPrefix = "Pembelian "
|
||||
)
|
||||
|
||||
// === CONTEXT STRUCTS ===
|
||||
|
||||
type CalculationContext struct {
|
||||
TotalPopulation float64
|
||||
TotalWeightProduced float64
|
||||
TotalEggWeightKg float64
|
||||
TotalDepletion float64
|
||||
TotalWeightSold float64
|
||||
ActualPopulation float64
|
||||
}
|
||||
|
||||
type ClosingKeuanganInput struct {
|
||||
ProjectFlockCategory string
|
||||
PurchaseItems []entities.PurchaseItem
|
||||
Budgets []entities.ProjectBudget
|
||||
Realizations []entities.ExpenseRealization
|
||||
DeliveryProducts []entities.MarketingDeliveryProduct
|
||||
Chickins []entities.ProjectChickin
|
||||
TotalWeightProduced float64
|
||||
TotalEggWeightKg float64
|
||||
TotalDepletion float64
|
||||
}
|
||||
|
||||
// === BASE METRICS ===
|
||||
|
||||
type FinancialMetrics struct {
|
||||
RpPerBird float64 `json:"rp_per_bird"`
|
||||
RpPerKg float64 `json:"rp_per_kg"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
type Comparison struct {
|
||||
Budgeting FinancialMetrics `json:"budgeting"`
|
||||
Realization FinancialMetrics `json:"realization"`
|
||||
}
|
||||
|
||||
// === HPP PURCHASES PACKAGE ===
|
||||
|
||||
type HppItem struct {
|
||||
Type string `json:"type"`
|
||||
Comparison
|
||||
}
|
||||
|
||||
type HppGroup struct {
|
||||
GroupName string `json:"group_name"`
|
||||
Data []HppItem `json:"data"`
|
||||
}
|
||||
|
||||
type SummaryHpp struct {
|
||||
Label string `json:"label"`
|
||||
Comparison `json:"-"`
|
||||
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
|
||||
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
|
||||
}
|
||||
|
||||
type HppPurchasesSection struct {
|
||||
Hpp []HppGroup `json:"hpp"`
|
||||
SummaryHpp SummaryHpp `json:"summary_hpp"`
|
||||
}
|
||||
|
||||
// === PROFIT LOSS PACKAGE ===
|
||||
|
||||
type PLItem struct {
|
||||
Type string `json:"type"`
|
||||
FinancialMetrics
|
||||
}
|
||||
|
||||
type PLSummaryItem struct {
|
||||
Label string `json:"label"`
|
||||
FinancialMetrics
|
||||
}
|
||||
|
||||
type PLSummaryGroup struct {
|
||||
GrossProfit PLSummaryItem `json:"gross_profit"`
|
||||
SubTotal PLSummaryItem `json:"sub_total"`
|
||||
NetProfit PLSummaryItem `json:"net_profit"`
|
||||
}
|
||||
|
||||
type ProfitLossData struct {
|
||||
Penjualan []PLItem `json:"penjualan"`
|
||||
Pembelian []PLItem `json:"pembelian"`
|
||||
Overhead PLItem `json:"overhead"`
|
||||
Ekspedisi PLItem `json:"ekspedisi"`
|
||||
Summary PLSummaryGroup `json:"summary"`
|
||||
}
|
||||
|
||||
type ProfitLossSection struct {
|
||||
Data ProfitLossData `json:"data"`
|
||||
}
|
||||
|
||||
// === RESPONSE DTO (ROOT) ===
|
||||
|
||||
type ReportResponse struct {
|
||||
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
|
||||
ProfitLoss ProfitLossSection `json:"profit_loss"`
|
||||
}
|
||||
|
||||
// === MAPPER FUNCTIONS ===
|
||||
|
||||
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
|
||||
return FinancialMetrics{
|
||||
RpPerBird: rpPerBird,
|
||||
RpPerKg: rpPerKg,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
|
||||
return Comparison{
|
||||
Budgeting: budgeting,
|
||||
Realization: realization,
|
||||
}
|
||||
}
|
||||
|
||||
// === HPP PENGELUARAN (from Purchase Items) ===
|
||||
|
||||
func getFlagLabel(flagType utils.FlagType) string {
|
||||
return PurchaseLabelPrefix + string(flagType)
|
||||
}
|
||||
|
||||
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
|
||||
flags := []utils.FlagType{
|
||||
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
|
||||
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
|
||||
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
|
||||
}
|
||||
|
||||
items := []HppItem{}
|
||||
seenFlags := make(map[utils.FlagType]bool)
|
||||
|
||||
for _, item := range purchaseItems {
|
||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, flag := range item.Product.Flags {
|
||||
flagType := utils.FlagType(flag.Name)
|
||||
|
||||
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
|
||||
amount := sumPurchasesByFlag(purchaseItems, flagType)
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
items = append(items, HppItem{
|
||||
Type: getFlagLabel(flagType),
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
|
||||
),
|
||||
})
|
||||
seenFlags[flagType] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
|
||||
|
||||
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
|
||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
return HppItem{
|
||||
Type: HPPLabelOverhead,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
|
||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
return HppItem{
|
||||
Type: HPPLabelEkspedisi,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
|
||||
items := []HppItem{}
|
||||
|
||||
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||
realizationAmount := getOperationalExpenses(realizations)
|
||||
|
||||
if budgetAmount > 0 || realizationAmount > 0 {
|
||||
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
|
||||
}
|
||||
|
||||
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
|
||||
|
||||
return HppGroup{
|
||||
GroupName: HPPGroupBahanBaku,
|
||||
Data: items,
|
||||
}
|
||||
}
|
||||
|
||||
// === HPP SUMMARY ===
|
||||
|
||||
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
|
||||
purchaseTotal := sumPurchaseTotal(purchaseItems)
|
||||
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
|
||||
totalBudget := purchaseTotal + budgetTotal
|
||||
|
||||
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
|
||||
|
||||
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
|
||||
|
||||
summary := SummaryHpp{
|
||||
Label: label,
|
||||
Comparison: ToComparison(
|
||||
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
|
||||
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
|
||||
),
|
||||
}
|
||||
|
||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
|
||||
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
|
||||
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
|
||||
|
||||
summary.EggBudgeting = &FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: budgetEggRpPerKg,
|
||||
Amount: totalBudget,
|
||||
}
|
||||
summary.EggRealization = &FinancialMetrics{
|
||||
RpPerBird: 0,
|
||||
RpPerKg: realizationEggRpPerKg,
|
||||
Amount: totalRealization,
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
|
||||
hppGroups := []HppGroup{
|
||||
{
|
||||
GroupName: HPPGroupPengeluaran,
|
||||
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
|
||||
},
|
||||
ToHppBahanBakuGroup(budgets, realizations, ctx),
|
||||
}
|
||||
|
||||
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
|
||||
|
||||
return HppPurchasesSection{
|
||||
Hpp: hppGroups,
|
||||
SummaryHpp: summaryHpp,
|
||||
}
|
||||
}
|
||||
|
||||
// === PROFIT & LOSS ===
|
||||
|
||||
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
|
||||
return PLItem{
|
||||
Type: itemType,
|
||||
FinancialMetrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
|
||||
return PLSummaryItem{
|
||||
Label: label,
|
||||
FinancialMetrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
|
||||
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||
}
|
||||
|
||||
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
|
||||
for _, item := range items {
|
||||
totalAmount += item.Amount
|
||||
totalPerBird += item.RpPerBird
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
|
||||
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
|
||||
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
|
||||
}
|
||||
|
||||
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
|
||||
items := []PLItem{}
|
||||
|
||||
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
|
||||
|
||||
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
|
||||
|
||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
|
||||
} else {
|
||||
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
|
||||
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
purchaseAmount := sumPurchaseTotal(purchases)
|
||||
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
realizationAmount := getOperationalExpenses(realizations)
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
|
||||
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
|
||||
return []PLItem{
|
||||
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
|
||||
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
|
||||
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
|
||||
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
|
||||
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
|
||||
|
||||
grossProfit := totalPenjualan - totalPembelian
|
||||
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
|
||||
|
||||
totalOtherExpenses := totalOverhead + totalEkspedisi
|
||||
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
|
||||
|
||||
netProfit := grossProfit - totalOtherExpenses
|
||||
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
|
||||
|
||||
return PLSummaryGroup{
|
||||
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
|
||||
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
|
||||
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
|
||||
}
|
||||
}
|
||||
|
||||
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
|
||||
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||
|
||||
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
|
||||
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
|
||||
|
||||
return ProfitLossData{
|
||||
Penjualan: penjualanItems,
|
||||
Pembelian: pembelianItems,
|
||||
Overhead: totalOverhead,
|
||||
Ekspedisi: totalEkspedisi,
|
||||
Summary: summary,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
|
||||
return ProfitLossSection{
|
||||
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
|
||||
}
|
||||
}
|
||||
|
||||
func aggregatePLItems(items []PLItem, label string) PLItem {
|
||||
totalAmount, totalPerBird := sumPLItems(items)
|
||||
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
|
||||
}
|
||||
|
||||
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
|
||||
return ReportResponse{
|
||||
HppPurchases: hppPurchases,
|
||||
ProfitLoss: profitLoss,
|
||||
}
|
||||
}
|
||||
|
||||
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
|
||||
var totalPopulation float64
|
||||
var totalWeightSold float64
|
||||
|
||||
for _, chickin := range input.Chickins {
|
||||
totalPopulation += chickin.UsageQty
|
||||
}
|
||||
|
||||
for _, delivery := range input.DeliveryProducts {
|
||||
totalWeightSold += delivery.TotalWeight
|
||||
}
|
||||
|
||||
ctx := CalculationContext{
|
||||
TotalPopulation: totalPopulation,
|
||||
TotalWeightProduced: input.TotalWeightProduced,
|
||||
TotalEggWeightKg: input.TotalEggWeightKg,
|
||||
TotalDepletion: input.TotalDepletion,
|
||||
TotalWeightSold: totalWeightSold,
|
||||
ActualPopulation: totalPopulation - input.TotalDepletion,
|
||||
}
|
||||
|
||||
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
|
||||
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
|
||||
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
|
||||
overheadItems := ToOverheadItems(input.Realizations, ctx)
|
||||
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
|
||||
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
|
||||
|
||||
return ToReportResponse(hppSection, plSection)
|
||||
}
|
||||
|
||||
// === HELPER FUNCTIONS ===
|
||||
|
||||
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
|
||||
if totalPopulation > 0 {
|
||||
rpPerBird = amount / totalPopulation
|
||||
}
|
||||
if totalWeightSold > 0 {
|
||||
rpPerKg = amount / totalWeightSold
|
||||
}
|
||||
return rpPerBird, rpPerKg
|
||||
}
|
||||
|
||||
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
|
||||
for _, flag := range flags {
|
||||
if strings.ToUpper(flag.Name) == string(flagType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
|
||||
return func(item *entities.PurchaseItem) bool {
|
||||
if item.Product == nil || len(item.Product.Flags) == 0 {
|
||||
return false
|
||||
}
|
||||
return hasProductFlag(item.Product.Flags, flagType)
|
||||
}
|
||||
}
|
||||
|
||||
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||
return func(realization *entities.ExpenseRealization) bool {
|
||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
|
||||
return false
|
||||
}
|
||||
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
|
||||
}
|
||||
}
|
||||
|
||||
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
|
||||
hasFlag := filterRealizationByNonstockFlag(flagType)
|
||||
return func(realization *entities.ExpenseRealization) bool {
|
||||
return !hasFlag(realization)
|
||||
}
|
||||
}
|
||||
|
||||
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
|
||||
amount := 0.0
|
||||
for i := range items {
|
||||
if filter(&items[i]) {
|
||||
amount += extractor(&items[i])
|
||||
}
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
|
||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
|
||||
}
|
||||
|
||||
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
|
||||
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
|
||||
}
|
||||
|
||||
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
|
||||
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
|
||||
}
|
||||
|
||||
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
|
||||
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
|
||||
}
|
||||
|
||||
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
|
||||
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
|
||||
}
|
||||
|
||||
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
|
||||
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
|
||||
}
|
||||
|
||||
func isChickenProductFlag(flagType utils.FlagType) bool {
|
||||
switch flagType {
|
||||
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
|
||||
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEggProductFlag(flagType utils.FlagType) bool {
|
||||
switch flagType {
|
||||
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
|
||||
utils.FlagTelurPutih, utils.FlagTelurRetak:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getSalesTypeFromProductFlags(product *entities.Product) string {
|
||||
if product == nil || len(product.Flags) == 0 {
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
|
||||
for _, flag := range product.Flags {
|
||||
flagType := utils.FlagType(strings.ToUpper(flag.Name))
|
||||
|
||||
if isEggProductFlag(flagType) {
|
||||
return PLSalesTypeEgg
|
||||
}
|
||||
if isChickenProductFlag(flagType) {
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
}
|
||||
|
||||
return PLSalesTypeChicken
|
||||
}
|
||||
|
||||
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
|
||||
categorized := make(map[string][]entities.MarketingDeliveryProduct)
|
||||
|
||||
for _, delivery := range deliveries {
|
||||
product := delivery.MarketingProduct.ProductWarehouse.Product
|
||||
salesType := getSalesTypeFromProductFlags(&product)
|
||||
|
||||
categorized[salesType] = append(categorized[salesType], delivery)
|
||||
}
|
||||
|
||||
return categorized
|
||||
}
|
||||
|
||||
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
|
||||
amount := 0.0
|
||||
for _, delivery := range deliveries {
|
||||
amount += delivery.TotalPrice
|
||||
}
|
||||
return amount
|
||||
}
|
||||
@@ -28,18 +28,14 @@ type SalesDTO struct {
|
||||
}
|
||||
|
||||
type PenjualanRealisasiResponseDTO struct {
|
||||
ProjectType string `json:"project_type"`
|
||||
FlockId uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
|
||||
// todo: usia ayam masih dummy
|
||||
age := 0
|
||||
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
|
||||
|
||||
var product *productDTO.ProductRelationDTO
|
||||
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||
@@ -68,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
DoNumber: doNumber,
|
||||
Product: product,
|
||||
Customer: customer,
|
||||
Qty: e.Qty,
|
||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
||||
Weight: e.TotalWeight,
|
||||
AvgWeight: e.AvgWeight,
|
||||
Price: e.UnitPrice,
|
||||
@@ -87,12 +83,10 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||
}
|
||||
|
||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
period := extractPeriodFromRealisasi(e)
|
||||
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
ProjectType: projectType,
|
||||
FlockId: projectFlockID,
|
||||
Period: period,
|
||||
Sales: ToSalesDTOs(e),
|
||||
|
||||
Sales: ToSalesDTOs(e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,3 +100,20 @@ func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
|
||||
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
|
||||
for _, chickin := range projectFlockKandang.Chickins {
|
||||
if chickin.ChickInDate.Before(earliestChickinDate) {
|
||||
earliestChickinDate = chickin.ChickInDate
|
||||
}
|
||||
}
|
||||
|
||||
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
|
||||
ageInWeeks := ageInDays / 7
|
||||
return ageInWeeks
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
|
||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||
latestDateByNonstockID := make(map[uint]string)
|
||||
|
||||
@@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
|
||||
for nonstockID, overhead := range overheadsByNonstockID {
|
||||
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||
|
||||
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation)
|
||||
|
||||
if overhead.ActualQuantity > 0 {
|
||||
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||
@@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
|
||||
BudgetTotalAmount: totalBudgetAmount,
|
||||
ActualQuantity: totalActualQuantity,
|
||||
ActualTotalAmount: totalActualAmount,
|
||||
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||
CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation),
|
||||
},
|
||||
Overheads: overheadItems,
|
||||
}
|
||||
@@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 {
|
||||
return qty * price
|
||||
}
|
||||
|
||||
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||
if totalChickinQty > 0 {
|
||||
return totalPrice / totalChickinQty
|
||||
func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 {
|
||||
if totalActualPopulation > 0 {
|
||||
return totalPrice / totalActualPopulation
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -30,10 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||
chickinRepo := rChickin.NewChickinRepository(db)
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ import (
|
||||
type ClosingRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlock]
|
||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
|
||||
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
|
||||
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
|
||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
||||
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
||||
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
@@ -24,6 +31,8 @@ type ClosingRepository interface {
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
}
|
||||
|
||||
type ClosingRepositoryImpl struct {
|
||||
@@ -53,6 +62,11 @@ type SapronakRow struct {
|
||||
Notes string `gorm:"column:notes"`
|
||||
}
|
||||
|
||||
type ExpeditionHPPRow struct {
|
||||
SupplierName string `gorm:"column:supplier_name"`
|
||||
TotalAmount float64 `gorm:"column:total_amount"`
|
||||
}
|
||||
|
||||
type SapronakQueryParams struct {
|
||||
Type string
|
||||
WarehouseIDs []uint
|
||||
@@ -111,6 +125,202 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
return rows, totalResults, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var purchaseAgg struct {
|
||||
TotalIn float64 `gorm:"column:total_in"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("purchase_items pi").
|
||||
Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'").
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(pi.total_qty), 0) AS total_in").
|
||||
Scan(&purchaseAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var usageAgg struct {
|
||||
TotalUsed float64 `gorm:"column:total_used"`
|
||||
}
|
||||
|
||||
err = r.DB().WithContext(ctx).
|
||||
Table("recording_stocks rs").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", "PAKAN").
|
||||
Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used").
|
||||
Scan(&usageAgg).Error
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
Total float64 `gorm:"column:total_culling"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_depletions rd").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name = ?", utils.FlagAyamCulling).
|
||||
Select("COALESCE(SUM(rd.qty), 0) AS total_culling").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.Total, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, 0, 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalWeight float64 `gorm:"column:total_weight"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("marketing_products mp").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) {
|
||||
if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var agg struct {
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs re").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products prod ON prod.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products").
|
||||
Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||
Where("f.name IN ?", flagNames).
|
||||
Select("COALESCE(SUM(re.qty), 0) AS total_qty").
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return agg.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
||||
if fcrID == 0 {
|
||||
return []entity.FcrStandard{}, nil
|
||||
}
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("fcr_id = ?", fcrID).
|
||||
Order("weight ASC").
|
||||
Find(&standards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return standards, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
if projectFlockID == 0 {
|
||||
return nil, fmt.Errorf("invalid project flock id")
|
||||
}
|
||||
|
||||
query := db.
|
||||
Table("expense_realizations AS er").
|
||||
Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id").
|
||||
Joins("JOIN expenses e ON e.id = ens.expense_id").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id").
|
||||
Joins("JOIN nonstocks n ON n.id = ens.nonstock_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||
Joins("JOIN suppliers s ON s.id = e.supplier_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("e.category = ?", "BOP").
|
||||
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
|
||||
|
||||
if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
|
||||
query = query.Where("pfk.id = ?", *projectFlockKandangID)
|
||||
}
|
||||
|
||||
var rows []ExpeditionHPPRow
|
||||
err := query.
|
||||
Select(
|
||||
"e.supplier_id AS supplier_id, " +
|
||||
"s.name AS supplier_name, " +
|
||||
"SUM(er.qty * er.price) AS total_amount",
|
||||
).
|
||||
Group("e.supplier_id, s.name").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
const (
|
||||
sapronakIncomingPurchasesSQL = `
|
||||
SELECT
|
||||
@@ -120,13 +330,33 @@ SELECT
|
||||
COALESCE(p.po_number, '') AS reference_number,
|
||||
'Purchase' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
'External Supplier' AS source_warehouse,
|
||||
'-' AS source_warehouse,
|
||||
w.name AS destination_warehouse,
|
||||
'' AS destination,
|
||||
pi.total_qty AS quantity,
|
||||
@@ -135,7 +365,6 @@ SELECT
|
||||
FROM purchase_items pi
|
||||
JOIN purchases p ON p.id = pi.purchase_id
|
||||
JOIN products prod ON prod.id = pi.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||
WHERE pi.warehouse_id IN ?
|
||||
@@ -149,16 +378,36 @@ SELECT
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer In' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.quantity AS quantity,
|
||||
std.usage_qty AS quantity,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -166,7 +415,6 @@ JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.to_warehouse_id IN ?
|
||||
`
|
||||
@@ -179,16 +427,36 @@ SELECT
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer Out' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
COALESCE(tw.name, '') AS destination,
|
||||
std.quantity AS quantity,
|
||||
std.usage_qty AS quantity,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
@@ -196,7 +464,6 @@ JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.from_warehouse_id IN ?
|
||||
`
|
||||
@@ -209,9 +476,29 @@ SELECT
|
||||
m.so_number AS reference_number,
|
||||
'Trading Sales' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
@@ -225,7 +512,6 @@ FROM marketing_products mp
|
||||
JOIN marketings m ON m.id = mp.marketing_id
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
WHERE pw.project_flock_kandang_id IN ?
|
||||
@@ -260,7 +546,6 @@ type SapronakDetailRow struct {
|
||||
Price float64
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||
|
||||
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||
@@ -323,7 +608,7 @@ func (r *ClosingRepositoryImpl) usageQuery(
|
||||
`)
|
||||
db = applyJoins(db, joins...)
|
||||
return db.
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN product_warehouses pw ON "+pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where(where, args...)
|
||||
@@ -356,7 +641,7 @@ func (r *ClosingRepositoryImpl) detailQuery(
|
||||
) *gorm.DB {
|
||||
db := r.withCtx(ctx).
|
||||
Table(table).
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN product_warehouses pw ON "+pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||
|
||||
@@ -450,7 +735,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Con
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||
return r.withCtx(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
@@ -527,7 +811,7 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui
|
||||
COALESCE(sl.increase,0) AS increase,
|
||||
COALESCE(sl.decrease,0) AS decrease,
|
||||
COALESCE(p.product_price,0) AS price,
|
||||
` + movementSelect + `
|
||||
`+movementSelect+`
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
@@ -577,7 +861,7 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -586,7 +870,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -597,4 +881,151 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand
|
||||
return fmt.Sprintf("TRF-%d", row.ID)
|
||||
})
|
||||
return in, out, nil
|
||||
}
|
||||
}
|
||||
|
||||
type ActualUsageCostRow struct {
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
FlagName string `gorm:"column:flag_name"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalPrice float64 `gorm:"column:total_price"`
|
||||
AveragePrice float64 `gorm:"column:average_price"`
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) {
|
||||
if projectFlockID == 0 {
|
||||
return []ActualUsageCostRow{}, nil
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
// Get all project flock kandang IDs for this project flock
|
||||
var pfkIDs []uint
|
||||
err := db.Table("project_flock_kandangs").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("id", &pfkIDs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pfkIDs) == 0 {
|
||||
return []ActualUsageCostRow{}, nil
|
||||
}
|
||||
|
||||
var rows []ActualUsageCostRow
|
||||
|
||||
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll)
|
||||
purchaseStockableKey := "PURCHASE_ITEMS"
|
||||
transferStockableKey := "STOCK_TRANSFER_DETAILS"
|
||||
|
||||
recordingQuery := db.
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
COALESCE(f.name, tf.name) AS flag_name,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_qty,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_price,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS qty_divisor,
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) / NULLIF(COALESCE(SUM(
|
||||
CASE
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
|
||||
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0), 0) AS average_price`,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey,
|
||||
purchaseStockableKey, transferStockableKey).
|
||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?",
|
||||
"recording_stocks", entity.StockAllocationStatusActive).
|
||||
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
|
||||
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
|
||||
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("r.project_flock_kandangs_id IN ?", pfkIDs).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Group("pw.product_id, p.name, COALESCE(f.name, tf.name)")
|
||||
|
||||
if err := recordingQuery.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Part 2: Get usage from project_chickins (DOC, Pullet)
|
||||
chickinQuery := db.
|
||||
Table("project_chickins AS pc").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag_name,
|
||||
COALESCE(SUM(pc.usage_qty), 0) AS total_qty,
|
||||
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS total_price,
|
||||
COALESCE(AVG(COALESCE(pi.price, 0)), 0) AS average_price
|
||||
`).
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = pc.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
|
||||
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where("pc.project_flock_kandang_id IN ?", pfkIDs).
|
||||
Where("pc.usage_qty > 0").
|
||||
Group("pw.product_id, p.name, f.name")
|
||||
|
||||
var chickinRows []ActualUsageCostRow
|
||||
if err := chickinQuery.Scan(&chickinRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge results
|
||||
rows = append(rows, chickinRows...)
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||
if len(productIDs) == 0 {
|
||||
return []entity.Product{}, nil
|
||||
}
|
||||
|
||||
var products []entity.Product
|
||||
err := r.DB().WithContext(ctx).
|
||||
Preload("Flags").
|
||||
Where("id IN ?", productIDs).
|
||||
Find(&products).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return products, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package closings
|
||||
|
||||
import (
|
||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
|
||||
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -13,6 +13,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||
|
||||
route := v1.Group("/closings")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
@@ -20,11 +21,15 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
|
||||
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
|
||||
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
|
||||
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
|
||||
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
|
||||
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
|
||||
route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
|
||||
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -15,6 +18,8 @@ import (
|
||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
@@ -30,7 +35,10 @@ type ClosingService interface {
|
||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error)
|
||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
|
||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||
}
|
||||
|
||||
type closingService struct {
|
||||
@@ -44,9 +52,11 @@ type closingService struct {
|
||||
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||
PurchaseRepo purchaseRepository.PurchaseRepository
|
||||
RecordingRepo recordingRepository.RecordingRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -58,6 +68,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||
ProjectBudgetRepo: projectBudgetRepo,
|
||||
ChickinRepo: chickinRepo,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
RecordingRepo: recordingRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +142,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit
|
||||
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
|
||||
Preload("MarketingProduct.Marketing").
|
||||
Preload("MarketingProduct.Marketing.Customer").
|
||||
Order("marketing_delivery_products.delivery_date DESC")
|
||||
@@ -320,18 +333,20 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
}
|
||||
|
||||
var (
|
||||
minStep uint16
|
||||
statusProject string
|
||||
completed int
|
||||
minStep uint16
|
||||
statusProject string
|
||||
completed int
|
||||
latestActionAt time.Time
|
||||
)
|
||||
|
||||
for _, rec := range records {
|
||||
if minStep == 0 || rec.StepNumber < minStep {
|
||||
minStep = rec.StepNumber
|
||||
statusProject = rec.StepName
|
||||
}
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||
completed++
|
||||
|
||||
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
|
||||
latestActionAt = rec.ActionAt
|
||||
statusProject = rec.StepName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +390,453 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
totalActualPopulation := totalChickinQty - totalDepletion
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
|
||||
}
|
||||
|
||||
// Get actual usage cost instead of purchase items
|
||||
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
|
||||
}
|
||||
|
||||
// Convert actual usage rows to pseudo purchase items
|
||||
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations")
|
||||
}
|
||||
|
||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.Product")
|
||||
})
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products")
|
||||
}
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins")
|
||||
}
|
||||
|
||||
totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
totalEggWeightKg, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalEggProductionWeightByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
|
||||
}
|
||||
|
||||
input := dto.ClosingKeuanganInput{
|
||||
ProjectFlockCategory: projectFlock.Category,
|
||||
PurchaseItems: purchaseItems,
|
||||
Budgets: budgets,
|
||||
Realizations: realizations,
|
||||
DeliveryProducts: deliveryProducts,
|
||||
Chickins: chickins,
|
||||
TotalWeightProduced: totalWeightProduced,
|
||||
TotalEggWeightKg: totalEggWeightKg,
|
||||
TotalDepletion: totalDepletion,
|
||||
}
|
||||
|
||||
report := dto.ToClosingKeuanganReport(input)
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP")
|
||||
}
|
||||
|
||||
expeditionCosts := make([]dto.ExpeditionCostItemDTO, 0, len(rows))
|
||||
var totalHPP float64
|
||||
|
||||
for idx, row := range rows {
|
||||
expeditionCosts = append(expeditionCosts, dto.ExpeditionCostItemDTO{
|
||||
Id: uint64(idx + 1),
|
||||
ExpeditionVendorName: row.SupplierName,
|
||||
HPPAmount: row.TotalAmount,
|
||||
})
|
||||
|
||||
totalHPP += row.TotalAmount
|
||||
}
|
||||
|
||||
result := &dto.ExpeditionHPPDTO{
|
||||
ExpeditionCosts: expeditionCosts,
|
||||
TotalHPPAmount: totalHPP,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get project flock %d for closing data produksi: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
var population float64
|
||||
for _, history := range project.KandangHistory {
|
||||
for _, chickin := range history.Chickins {
|
||||
population += chickin.UsageQty + chickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
|
||||
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
|
||||
|
||||
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
|
||||
}
|
||||
|
||||
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
|
||||
}
|
||||
|
||||
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data")
|
||||
}
|
||||
|
||||
finalPopulation := population - claimCulling
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if project.FcrId > 0 {
|
||||
standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
|
||||
}
|
||||
}
|
||||
age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
|
||||
}
|
||||
|
||||
feedUsedPerHead := 0.0
|
||||
if population > 0 {
|
||||
feedUsedPerHead = feedUsed / population
|
||||
}
|
||||
|
||||
purchase := dto.ClosingPurchaseDTO{
|
||||
InitialPopulation: int(population),
|
||||
ClaimCulling: int(claimCulling),
|
||||
FinalPopulation: int(finalPopulation),
|
||||
FeedIn: feedIn,
|
||||
FeedUsed: feedUsed,
|
||||
FeedUsedPerHead: feedUsedPerHead,
|
||||
}
|
||||
|
||||
chickenFlagNames := []string{string(utils.FlagPullet)}
|
||||
chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data")
|
||||
}
|
||||
|
||||
var chickenAverageWeight float64
|
||||
if chickenSalesQty > 0 {
|
||||
chickenAverageWeight = chickenSalesWeight / chickenSalesQty
|
||||
}
|
||||
|
||||
var chickenAverageSellingPrice float64
|
||||
if chickenSalesWeight > 0 {
|
||||
chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight
|
||||
}
|
||||
|
||||
chickenSales := dto.ClosingSalesDTO{
|
||||
SalesPopulation: int(chickenSalesQty),
|
||||
SalesWeight: chickenSalesWeight,
|
||||
AverageWeight: chickenAverageWeight,
|
||||
AverageSellingPrice: chickenAverageSellingPrice,
|
||||
}
|
||||
|
||||
chickenDepletion := population - chickenSalesQty
|
||||
if chickenDepletion < 0 {
|
||||
chickenDepletion = 0
|
||||
}
|
||||
|
||||
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
|
||||
|
||||
var eggSales *dto.ClosingEggSalesDTO
|
||||
var eggPerformance *dto.ClosingPerformanceDTO
|
||||
if !isGrowing {
|
||||
eggFlagNames := []string{
|
||||
string(utils.FlagTelur),
|
||||
string(utils.FlagTelurUtuh),
|
||||
string(utils.FlagTelurPecah),
|
||||
string(utils.FlagTelurPutih),
|
||||
string(utils.FlagTelurRetak),
|
||||
}
|
||||
|
||||
eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data")
|
||||
}
|
||||
|
||||
var averageEggWeight float64
|
||||
if eggSalesQty > 0 {
|
||||
averageEggWeight = eggSalesWeight / eggSalesQty
|
||||
}
|
||||
|
||||
var averageEggSellingPrice float64
|
||||
if eggSalesWeight > 0 {
|
||||
averageEggSellingPrice = eggSalesPrice / eggSalesWeight
|
||||
}
|
||||
|
||||
eggSales = &dto.ClosingEggSalesDTO{
|
||||
EggPieces: int(eggSalesQty),
|
||||
EggMassKg: eggSalesWeight,
|
||||
AverageEggWeightKg: averageEggWeight,
|
||||
AverageSellingPrice: averageEggSellingPrice,
|
||||
}
|
||||
|
||||
harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data")
|
||||
}
|
||||
|
||||
eggDepletion := harvestEggQty - eggSalesQty
|
||||
if eggDepletion < 0 {
|
||||
eggDepletion = 0
|
||||
}
|
||||
|
||||
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
|
||||
eggPerformance = &eggPerf
|
||||
}
|
||||
|
||||
sales := dto.ClosingSalesGroupDTO{
|
||||
Chicken: chickenSales,
|
||||
Egg: eggSales,
|
||||
}
|
||||
|
||||
performance := dto.ClosingPerformanceDTO{
|
||||
Depletion: chickenPerformance.Depletion,
|
||||
Age: age,
|
||||
MortalityStd: chickenPerformance.MortalityStd,
|
||||
MortalityAct: chickenPerformance.MortalityAct,
|
||||
DeffMortality: chickenPerformance.DeffMortality,
|
||||
}
|
||||
if eggPerformance != nil {
|
||||
performance.FcrStd = eggPerformance.FcrStd
|
||||
performance.FcrAct = eggPerformance.FcrAct
|
||||
performance.DeffFcr = eggPerformance.DeffFcr
|
||||
performance.Awg = eggPerformance.Awg
|
||||
} else {
|
||||
performance.FcrStd = chickenPerformance.FcrStd
|
||||
performance.FcrAct = chickenPerformance.FcrAct
|
||||
performance.DeffFcr = chickenPerformance.DeffFcr
|
||||
performance.Awg = chickenPerformance.Awg
|
||||
}
|
||||
|
||||
result := dto.ClosingProductionReportDTO{
|
||||
Purchase: purchase,
|
||||
Sales: sales,
|
||||
Performance: performance,
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) {
|
||||
deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("MarketingProduct").
|
||||
Preload("MarketingProduct.ProductWarehouse").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins")
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
totalQty float64
|
||||
totalAgeWeeks float64
|
||||
)
|
||||
|
||||
for _, product := range deliveryProducts {
|
||||
if product.UsageQty == 0 {
|
||||
continue
|
||||
}
|
||||
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
|
||||
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
|
||||
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
|
||||
totalQty += product.UsageQty
|
||||
}
|
||||
|
||||
if totalQty == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return totalAgeWeeks / totalQty, nil
|
||||
}
|
||||
|
||||
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
|
||||
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
|
||||
|
||||
fcrAct := 0.0
|
||||
if totalWeight > 0 {
|
||||
fcrAct = feedUsed / totalWeight
|
||||
}
|
||||
|
||||
mortalityAct := 0.0
|
||||
if basePopulation > 0 {
|
||||
mortalityAct = (depletion / basePopulation) * 100
|
||||
}
|
||||
|
||||
deffMortality := mortalityAct - mortalityStd
|
||||
deffFcr := fcrAct - fcrStd
|
||||
|
||||
awg := 0.0
|
||||
if age > 0 {
|
||||
awg = averageWeight / age
|
||||
}
|
||||
|
||||
return dto.ClosingPerformanceDTO{
|
||||
Depletion: depletion,
|
||||
Age: age,
|
||||
MortalityStd: mortalityStd,
|
||||
MortalityAct: mortalityAct,
|
||||
DeffMortality: deffMortality,
|
||||
FcrStd: fcrStd,
|
||||
FcrAct: fcrAct,
|
||||
DeffFcr: deffFcr,
|
||||
Awg: awg,
|
||||
}
|
||||
}
|
||||
|
||||
func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) {
|
||||
if len(standards) == 0 || averageWeight <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
closest := standards[0]
|
||||
minDiff := math.Abs(closest.Weight - averageWeight)
|
||||
for _, std := range standards[1:] {
|
||||
diff := math.Abs(std.Weight - averageWeight)
|
||||
if diff < minDiff {
|
||||
minDiff = diff
|
||||
closest = std
|
||||
}
|
||||
}
|
||||
|
||||
return closest.Mortality, closest.FcrNumber
|
||||
}
|
||||
|
||||
func (s closingService) convertActualUsageToPurchaseItems(ctx context.Context, actualUsageRows []repository.ActualUsageCostRow) []entity.PurchaseItem {
|
||||
if len(actualUsageRows) == 0 {
|
||||
return []entity.PurchaseItem{}
|
||||
}
|
||||
|
||||
// Collect all product IDs
|
||||
productIDs := make([]uint, len(actualUsageRows))
|
||||
for i, row := range actualUsageRows {
|
||||
productIDs[i] = row.ProductID
|
||||
}
|
||||
|
||||
// Fetch products with flags from repository
|
||||
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch products for actual usage: %v", err)
|
||||
products = []entity.Product{}
|
||||
}
|
||||
|
||||
// Create product map
|
||||
productMap := make(map[uint]*entity.Product)
|
||||
for i := range products {
|
||||
productMap[products[i].Id] = &products[i]
|
||||
}
|
||||
|
||||
// Convert to pseudo purchase items
|
||||
purchaseItems := make([]entity.PurchaseItem, 0, len(actualUsageRows))
|
||||
for _, row := range actualUsageRows {
|
||||
product := productMap[row.ProductID]
|
||||
|
||||
// Skip if product not found
|
||||
if product == nil {
|
||||
s.Log.Warnf("Product ID %d not found for actual usage", row.ProductID)
|
||||
continue
|
||||
}
|
||||
|
||||
purchaseItem := entity.PurchaseItem{
|
||||
Id: 0, // Pseudo item, no ID
|
||||
ProductId: row.ProductID,
|
||||
TotalQty: row.TotalQty,
|
||||
TotalPrice: row.TotalPrice,
|
||||
Price: row.AveragePrice,
|
||||
Product: product,
|
||||
}
|
||||
|
||||
purchaseItems = append(purchaseItems, purchaseItem)
|
||||
}
|
||||
|
||||
return purchaseItems
|
||||
}
|
||||
|
||||
@@ -12,6 +12,5 @@ func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) {
|
||||
ctrl := controller.NewConstantController(s)
|
||||
|
||||
route := v1.Group("/constants")
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user