Compare commits

...

365 Commits

Author SHA1 Message Date
giovanni fa928d97a8 first commit 2026-01-27 11:10:16 +07:00
Adnan Zahir 3c77aff413 Merge branch 'dev/teguh' into 'development'
FEAT[BE]: transfer to laying, stock transfer, refactor adjustment, fix  age closing penjualan, filter transfer to laying

See merge request mbugroup/lti-api!256
2026-01-27 10:17:00 +07:00
Hafizh A. Y. f8d42dbdb3 Merge branch 'fix/BE/Report-purchasing-Debt-supplier-and-Closing-counting-sapronak' into 'development'
[FIX/BE-US] fix closing count sapronak,expense notes purchase

See merge request mbugroup/lti-api!255
2026-01-27 03:16:34 +00:00
Adnan Zahir e881c2b952 Merge branch 'FIX/BE/Closing_keuangan' into 'development'
[FIX]BE: fixing and refactoring closing keuangan to use hpp service and repo for some getter data

See merge request mbugroup/lti-api!254
2026-01-27 10:16:01 +07:00
Adnan Zahir 52ebcc5c2d Merge branch 'fix/hpp-calculate' into 'development'
[FIX][BE]: adjust calculate hpp filter delivery date marketing delivery

See merge request mbugroup/lti-api!253
2026-01-27 10:09:19 +07:00
aguhh18 3e0291c2ba Feat[BE]: enhance transfer laying functionality with comprehensive filtering options and improved DTO structures 2026-01-26 23:50:04 +07:00
aguhh18 7a704c4ec4 Feat[BE]: implement max target quantity retrieval for kandangs and update routes 2026-01-26 18:03:54 +07:00
ragilap bd0f89c521 [FIX/BE-US] fix closing count sapronak,expense notes purchase 2026-01-26 16:47:28 +07:00
aguhh18 b83ebc0ff9 Feat[BE] : add stock log to transfer service 2026-01-26 16:26:59 +07:00
aguhh18 1572dfd0b8 Refactor[BE] adjustment stock handling: remove stock_log_id, update relations, and enhance transfer logging 2026-01-26 16:26:20 +07:00
aguhh18 258fd1d7e0 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-26 13:40:17 +07:00
aguhh18 f44ddef79b Fix[BE]: update age calculation logic to include additional product flags and start day laying form week 18 2026-01-26 13:39:56 +07:00
aguhh18 9339e1e9f0 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FIX/BE/Closing_keuangan 2026-01-26 13:01:16 +07:00
aguhh18 798dd7f9a3 Fix[BE]: fixing and refactoring closing keuangan to use hpp service and repo for some getter data 2026-01-26 13:00:30 +07:00
giovanni 7a8f813e1f adjust calculate hpp filter delivery date marketing delivery 2026-01-26 11:44:22 +07:00
Hafizh A. Y. fdd8e3ec31 Merge branch 'fix/hpp-calculate' into 'development'
[FIX][BE]: fix calculate sisa berat telur

See merge request mbugroup/lti-api!251
2026-01-24 09:34:24 +00:00
Hafizh A. Y. 0f6cd3a054 Merge branch 'fix/BE/UUIT-Recording-closing-report-uniformity-dashboard' into 'development'
Fix/be/uuit recording and dashboard

See merge request mbugroup/lti-api!250
2026-01-24 09:34:07 +00:00
Hafizh A. Y. c6d087eeab Merge branch 'fix/customer-finance' into 'development'
[FIX][BE] Edit customer, finance: bank optional, nominal minus, and filter

See merge request mbugroup/lti-api!249
2026-01-24 09:31:09 +00:00
Hafizh A. Y. 437cd3beda Merge branch 'fix/warehouse' into 'development'
[FIX][BE]: add filter location id

See merge request mbugroup/lti-api!248
2026-01-24 09:30:49 +00:00
ragilap 8fbce5a01e Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/UUIT-Recording-closing-report-uniformity-dashboard 2026-01-24 14:20:30 +07:00
ragilap f4b2408698 [FIX/BE-US] fix recording stock and dashboard filtering 2026-01-24 14:20:14 +07:00
giovanni 8c84981812 adjust get weight remaining 2026-01-24 14:17:50 +07:00
Hafizh A. Y 458c8e0a91 fix(BE): edit customer, finance: bank optional, nominal minus, and filter 2026-01-24 13:35:13 +07:00
giovanni 4646bf5577 add filter location id 2026-01-24 13:34:21 +07:00
giovanni 8b1831fc73 adjust take weight remaining 2026-01-24 12:03:09 +07:00
Adnan Zahir 919dc5c2e8 Merge branch 'dev/teguh' into 'development'
FIX[BE]: module transfer, report penjualan, closing penjualan

See merge request mbugroup/lti-api!244
2026-01-24 12:01:47 +07:00
Adnan Zahir 129d253683 Merge branch 'fix/hpp-calculate' into 'development'
[FIX][BE]: fix HPP get egg sales pieces and weight

See merge request mbugroup/lti-api!245
2026-01-24 11:38:31 +07:00
giovanni f69321d9cd fix get egg sales pieces and weight 2026-01-24 11:25:11 +07:00
aguhh18 74158138c0 Fix[BE]: enhance error logging and messages in transfer service 2026-01-24 10:38:52 +07:00
aguhh18 699a6e9289 Fix[BE]: update error message for insufficient stock in adjustment service 2026-01-24 09:40:00 +07:00
aguhh18 507d6c4293 FEAT[BE]: implement HPP on penjualan harian 2026-01-23 22:02:19 +07:00
aguhh18 43286cead1 Fix[BE]: fixing transfer to non kandang warehouse 2026-01-23 19:45:13 +07:00
aguhh18 1216e65419 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-23 17:54:58 +07:00
Adnan Zahir 42f030a780 Merge branch 'fix/data-production' into 'development'
[FIX][BE]: fix value all standard

See merge request mbugroup/lti-api!243
2026-01-23 16:27:54 +07:00
Adnan Zahir cf475d678e Merge branch 'fix/BE/Report-purchasing-Debt-supplier-and-Closing-counting-sapronak' into 'development'
[FIX/BE-US] fix closing counting sapronak

See merge request mbugroup/lti-api!242
2026-01-23 16:20:27 +07:00
Adnan Zahir a42c201ac6 Merge branch 'fix/hpp-calculate' into 'development'
[FIX][BE]: create common service calculate hpp

See merge request mbugroup/lti-api!241
2026-01-23 16:16:52 +07:00
Adnan Zahir d4699fba5b Merge branch 'fix/BE/UUIT-Recording-closing-report-uniformity-dashboard' into 'development'
Fix/be/uuit recording closing report uniformity dashboard

See merge request mbugroup/lti-api!240
2026-01-23 16:11:06 +07:00
giovanni e1ab5a90cb fix value all standard 2026-01-23 14:20:11 +07:00
aguhh18 f060da1cd3 FIX[BE]: Integrate StockLogRepository into deliveryOrdersService for stock logging functionality 2026-01-23 12:35:34 +07:00
ragilap f82ac01e7c [FIX/BE-US] fix recording date 2026-01-23 12:02:26 +07:00
ragilap fd2f773806 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/UUIT-Recording-closing-report-uniformity-dashboard 2026-01-23 11:57:38 +07:00
ragilap 7db3afe985 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/Report-purchasing-Debt-supplier-and-Closing-counting-sapronak 2026-01-23 11:53:54 +07:00
ragilap 8dc88b97a4 [FIX/BE-US] fix closing counting sapronak 2026-01-23 11:43:50 +07:00
aguhh18 f1787d3375 FIX[BE]: Fix wrong calculation avg sales on report penjualan harian 2026-01-23 11:07:11 +07:00
aguhh18 6b4eb758e4 FIX[BE]: fixing umur on closing penjualan for penjualan OVK and PAKAN 2026-01-23 10:32:40 +07:00
giovanni d54911f8b4 adjust value hpp 2026-01-23 10:29:48 +07:00
giovanni 1edd071a8a Merge branch 'development' into fix/hpp-calculate 2026-01-23 10:24:50 +07:00
giovanni fb565ef728 fix hpp harian kandang 2026-01-23 10:01:48 +07:00
ragilap 9928b4c970 [FIX/BE-US] fix uniformity relation chickin date 2026-01-22 17:50:04 +07:00
ragilap 8c58cc4103 [FIX/BE-US] fix uniformity relation chickin date 2026-01-22 17:49:46 +07:00
giovanni 0d585a99a6 adjust api hpp per kandang and implement common service hpp 2026-01-22 17:37:28 +07:00
M1 AIR b1b50c3c01 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into development 2026-01-22 15:57:47 +07:00
M1 AIR c085888ca9 Update cicd no migrate prod 2026-01-22 15:57:16 +07:00
Adnan Zahir 774c9b1d58 Merge branch 'fix/BE/US-281-adjustment-recording' into 'development'
[FIX/BE-US] fix day in recording

See merge request mbugroup/lti-api!237
2026-01-22 14:47:40 +07:00
ragilap 12ed9cd753 [FIX/BE-US] fix day in recording 2026-01-22 14:44:53 +07:00
Hafizh A. Y. 4ab1553340 Merge branch 'HOTFIX/BE/marketing' into 'development'
Hotfix/be/marketing : hotfix perhitungan penjualan ovk dan pakan, overhead only bop

See merge request mbugroup/lti-api!235
2026-01-22 07:43:10 +00:00
Hafizh A. Y. 3fa50b6344 Merge branch 'fix/BE/Report-purchasing-Debt-supplier-and-Closing-counting-sapronak' into 'development'
[FIX/BE-US] debt-supplier only show receive purchase

See merge request mbugroup/lti-api!232
2026-01-22 07:42:19 +00:00
Hafizh A. Y. 80424dee17 Merge branch 'fix/production-result' into 'development'
[FIX][BE]: remove max limit production result

See merge request mbugroup/lti-api!230
2026-01-22 07:42:02 +00:00
Hafizh A. Y. a9b33eaf28 Merge branch 'fix/BE/Purchase-edit-qty' into 'development'
[FIX/BE-US] adjustment recording and purchase stock log

See merge request mbugroup/lti-api!229
2026-01-22 07:41:50 +00:00
aguhh18 551534d02a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into HOTFIX/BE/marketing 2026-01-22 14:32:39 +07:00
aguhh18 202a8ffc66 HOTFIX[BE]: filter closing overhead by expense category "BOP" 2026-01-22 14:29:56 +07:00
giovanni 6bc5e7d293 fix get average bw 2026-01-22 14:19:16 +07:00
ragilap 2e0827dec5 [FIX/BE-US] debt-supplier only show receive and changes lunas 2026-01-22 14:15:26 +07:00
aguhh18 87973a6c9f HOTFIX[BE]: update total price calculation based on product flags for delivery and sales orders 2026-01-22 14:15:03 +07:00
M1 AIR 1ca6c6a104 Fixing staging cicd 2026-01-22 14:12:20 +07:00
M1 AIR 78a45b11e7 fixing pipeline 2026-01-22 13:59:52 +07:00
giovanni 58b29501c0 finishing common service calculate hpp 2026-01-22 13:54:32 +07:00
ragilap bac0361df5 [FIX/BE-US] debt-supplier only show receive purchase 2026-01-22 13:53:43 +07:00
giovanni 645a97b460 remove max limit production result 2026-01-22 13:42:36 +07:00
ragilap eb2479cc41 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/Purchase-edit-qty 2026-01-22 13:36:53 +07:00
ragilap 04ec8560a7 [FIX/BE-US] adjustment recording and purchase stock log 2026-01-22 13:35:46 +07:00
Hafizh A. Y. 12240a9e2d Merge branch 'fix/mr-development-staging' into 'development'
Fix/mr development staging

See merge request mbugroup/lti-api!228
2026-01-22 06:17:31 +00:00
giovanni f2a46843c8 continue common service hpp 2026-01-22 12:53:02 +07:00
aguhh18 06e92d1c77 [FEAT][BE}: add umur week and day on closing penjualan 2026-01-22 11:17:02 +07:00
giovanni d7ed768d14 add filter project status and location id 2026-01-22 11:06:17 +07:00
ragilap f04cbd24bd [FIX/BE-US] adjustment recording 2026-01-22 11:06:17 +07:00
aguhh18 ec4b849778 FIX[BE]: fixing wrong index data on adjustment. change get from stocklogs to adjustment table 2026-01-22 11:06:17 +07:00
aguhh18 132e043597 FEAT[BE]: update warehouse DTO references in product warehouse and add UOM preload 2026-01-22 11:06:17 +07:00
aguhh18 e99af36796 FIX[BE] fix wrong calculation on summary report marketing 2026-01-22 11:06:17 +07:00
aguhh18 f6bdb17699 FIX[BE]: fixing report penjualan add avg weight and price to response 2026-01-22 11:06:17 +07:00
aguhh18 8f7fc622f6 FIX[BE]: fixing closing penjualan add sumary 2026-01-22 11:06:17 +07:00
aguhh18 30f5ed417c FEAT[BE]: add default filterby become so_date in report markeing 2026-01-22 11:06:17 +07:00
aguhh18 1d726afa6f FEAT[BE[: enhance marketing report items with aging days calculation 2026-01-22 11:06:17 +07:00
aguhh18 96ba947952 FEAT[BE[: add avg weight and avg amount on get penjualan harian 2026-01-22 11:06:17 +07:00
aguhh18 ad0504f49e refactor: unify GetOne method to return approval alongside transfer laying 2026-01-22 11:06:17 +07:00
giovanni 0b708cd57b fix data produksi not show response 2026-01-22 11:06:17 +07:00
ragilap 32153f02b8 [FIX/BE-US] purchase edit qty approval staf add adjustment fifo system 2026-01-22 11:06:17 +07:00
M1 AIR cf37822a07 Change rules cicd no conflicts 2026-01-22 11:06:16 +07:00
Hafizh A. Y. 7fb6c3c7bf Merge branch 'fix/sapronak' into 'development'
[FIX][BE]: add filter project status and location id

See merge request mbugroup/lti-api!226
2026-01-22 03:09:32 +00:00
giovanni fd689919b0 Merge branch 'development' into fix/hpp-calculate 2026-01-22 10:05:08 +07:00
giovanni 2ad0c17fbe add filter project status and location id 2026-01-22 10:00:24 +07:00
ragilap e8c7b3f2a8 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/Purchase-edit-qty 2026-01-22 09:50:21 +07:00
Hafizh A. Y. ce09c2473c Merge branch 'Fix/BE/US-281-adjustment-recording' into 'development'
[FIX/BE-US] adjustment recording

See merge request mbugroup/lti-api!222
2026-01-22 02:49:38 +00:00
Hafizh A. Y. 3493d1d7b2 Merge branch 'dev/teguh' into 'development'
[FIX][BE]: fixing bug after SIT module report marketing and closing marketing

See merge request mbugroup/lti-api!220
2026-01-22 02:48:41 +00:00
Hafizh A. Y. 9fff954857 Merge branch 'fix/LSS416' into 'development'
[FIX][BE]: fix data produksi not show response

See merge request mbugroup/lti-api!219
2026-01-22 02:48:13 +00:00
Hafizh A. Y. c7c1e4b335 Merge branch 'fix/BE/Purchase-edit-qty' into 'development'
[FIX/BE-US] purchase edit qty approval staf & add adjustment fifo system

See merge request mbugroup/lti-api!218
2026-01-22 02:47:35 +00:00
ragilap 5dbe9bb989 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE/Purchase-edit-qty 2026-01-22 09:33:15 +07:00
aguhh18 e555cfa950 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-22 09:27:17 +07:00
M1 AIR 32a8557a3b Change rules cicd no conflicts 2026-01-21 15:56:58 +07:00
M1 AIR 1818f5a295 merge: sync staging with production 2026-01-21 15:16:36 +07:00
aguhh18 a73b44808f FIX[BE]: fixing wrong index data on adjustment. change get from stocklogs to adjustment table 2026-01-21 15:16:30 +07:00
Adnan Zahir 69d9137e78 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!224
2026-01-21 14:55:09 +07:00
Adnan Zahir e32b231c6c Merge branch 'fix/daily-checklist' into 'development'
fix

See merge request mbugroup/lti-api!223
2026-01-21 14:25:26 +07:00
giovanni ca6d0b160b fix 2026-01-21 14:17:43 +07:00
aguhh18 e8a89f0f17 FEAT[BE]: update warehouse DTO references in product warehouse and add UOM preload 2026-01-21 13:52:46 +07:00
giovanni d96a12776a next commit 2026-01-21 13:38:59 +07:00
Adnan Zahir adabb4e035 Merge branch 'fix/daily-checklist' into 'development'
[FIX][BE]: fix duplicate name, time type and phase id create phase activity

See merge request mbugroup/lti-api!221
2026-01-21 13:30:21 +07:00
ragilap 16a0b848bc [FIX/BE-US] adjustment recording 2026-01-21 13:06:45 +07:00
aguhh18 c2d2701d72 FIX[BE] fix wrong calculation on summary report marketing 2026-01-21 09:57:44 +07:00
aguhh18 894efa7aa5 FIX[BE]: fixing report penjualan add avg weight and price to response 2026-01-21 09:46:03 +07:00
aguhh18 d0625e7d21 FIX[BE]: fixing closing penjualan add sumary 2026-01-21 09:45:19 +07:00
giovanni 67ecdbc1dd fix duplicate name, time type and phase id create phase activity 2026-01-20 23:01:58 +07:00
aguhh18 d50ab7cc97 FEAT[BE]: add default filterby become so_date in report markeing 2026-01-20 22:42:16 +07:00
aguhh18 ad3bb0e29a FEAT[BE[: enhance marketing report items with aging days calculation 2026-01-20 22:28:34 +07:00
aguhh18 dd4dcc1c39 FEAT[BE[: add avg weight and avg amount on get penjualan harian 2026-01-20 22:10:47 +07:00
aguhh18 aa4da68680 refactor: unify GetOne method to return approval alongside transfer laying 2026-01-20 22:07:07 +07:00
giovanni 95965cb26a add common service and repo for calculate hpp 2026-01-20 18:21:49 +07:00
giovanni e4e17f16f9 fix data produksi not show response 2026-01-20 18:18:41 +07:00
ragilap edd77c5265 [FIX/BE-US] purchase edit qty approval staf add adjustment fifo system 2026-01-20 16:40:37 +07:00
Adnan Zahir dab692a0c1 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!217
2026-01-20 14:25:14 +07:00
Hafizh A. Y. e6244dea8a Merge branch 'Fix/BE/Purchase-edit-qty' into 'development'
[FIX/BE-US] purchase edit qty approval staf

See merge request mbugroup/lti-api!216
2026-01-20 07:21:27 +00:00
Hafizh A. Y. d1e4bf060e Merge branch 'fix/LSS416' into 'development'
[FIX][BE]: fix endpoint not found

See merge request mbugroup/lti-api!213
2026-01-20 07:21:16 +00:00
ragilap fc06b3e4db [FIX/BE-US] purchase edit qty approval staf 2026-01-20 14:19:02 +07:00
Adnan Zahir cf42e8c130 Merge branch 'staging' into 'production'
Staging

See merge request mbugroup/lti-api!215
2026-01-20 12:11:41 +07:00
Adnan Zahir 8ff97cb647 Merge branch 'revert-21de17b1' into 'development'
Revert "Merge branch 'staging' into 'development'"

See merge request mbugroup/lti-api!214
2026-01-20 12:08:53 +07:00
Adnan Zahir 1b7ce3c62c Revert "Merge branch 'staging' into 'development'"
This reverts merge request !212
2026-01-20 12:08:41 +07:00
Adnan Zahir 21de17b18a Merge branch 'staging' into 'development'
Staging

See merge request mbugroup/lti-api!212
2026-01-20 12:07:52 +07:00
giovanni 2aaaab91f7 fix endpoint not found 2026-01-20 12:07:16 +07:00
Adnan Zahir 4227152979 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!209
2026-01-20 11:56:00 +07:00
Hafizh A. Y. ec020ac17c Merge branch 'Fix/BE/UUIT-Recording-closing-report-uniformity-dashboard' into 'development'
[FIX/BE-US] recording,reporting,closing and uniformity

See merge request mbugroup/lti-api!211
2026-01-20 03:55:35 +00:00
Hafizh A. Y. adc30ad5cd Merge branch 'fix/LSS416' into 'development'
[FIX][BE]: adjust closing tap sapronak; add api summart total kuantitas per category and uom

See merge request mbugroup/lti-api!210
2026-01-20 03:54:47 +00:00
ragilap 9fb5395469 [FIX/BE-US] recording,reporting,closing and uniformity 2026-01-20 10:13:58 +07:00
giovanni bc771660be adjust closing tap sapronak; add api summart total kuantitas per category and uom 2026-01-20 10:03:57 +07:00
Hafizh A. Y. b615570036 Merge branch 'fix/LSS390' into 'development'
[FIX][BE]: LSS390

See merge request mbugroup/lti-api!208
2026-01-20 02:23:42 +00:00
Hafizh A. Y. c793c3cf9a Merge branch 'dev/teguh' into 'development'
FIX[BE]: fixing BE

See merge request mbugroup/lti-api!207
2026-01-20 02:23:29 +00:00
aguhh18 b3e0410f5a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-20 09:15:21 +07:00
Hafizh A. Y. a882d5a687 Merge branch 'Feat/BE/US-Closing_finance' into 'development'
[FIX][BE]: fixing filter on report penjualan and fix stock movement to not strict to not kandang warehouse

See merge request mbugroup/lti-api!206
2026-01-20 01:41:15 +00:00
Hafizh A. Y. c0848b6d2d Merge branch 'fix/closing-sapronak' into 'development'
[FIX][BE]: query outgoing sapronak

See merge request mbugroup/lti-api!205
2026-01-20 01:40:35 +00:00
aguhh18 b240478ed5 feat[BE]: Add notes field to Update validation and update approval logic in expense services 2026-01-19 17:44:10 +07:00
giovanni 3052497fc0 adjust grouping by project flock kandang 2026-01-19 17:05:43 +07:00
giovanni 71c62c5e02 [FIX][BE]: LSS390 2026-01-19 16:19:47 +07:00
aguhh18 768961d7d6 fix[BE]: Refactor GetAll method to improve query parameter handling and formatting 2026-01-19 14:39:43 +07:00
aguhh18 8cd9627a51 feat[BE]: Add requested_qty field to LayingTransferSource and update related logic for transfer operations 2026-01-19 14:34:08 +07:00
aguhh18 378d633ea4 feat[BE]: Enhance payment allocation logic to support FIFO consumption for sales transactions 2026-01-19 09:27:37 +07:00
aguhh18 fb193fc61f fix[BE]: Update GetAllWithFilters to enhance search functionality and join conditions 2026-01-18 21:26:31 +07:00
aguhh18 af7aabdec8 fix[BE]: Adjust validation for MarketingQuery limit to remove max constraint 2026-01-18 20:50:52 +07:00
aguhh18 bac36b4f00 fix[BE]: Update error messages in TransferService to provide clearer context in Indonesian 2026-01-18 20:36:33 +07:00
aguhh18 687d02313b feat[BE]: Update TransferRelationDTO and service search logic to include warehouse names 2026-01-18 19:46:09 +07:00
aguhh18 7d3602d829 feat[BE]: Enhance CreateOne method to validate project flock closing status and handle warehouse without kandang_id 2026-01-17 13:22:01 +07:00
aguhh18 533e9aca6f FIX[BE]: Fixing filter area and location 2026-01-17 12:20:40 +07:00
giovanni dcfb5e10b4 adjust max limit to 1000 2026-01-17 11:34:12 +07:00
giovanni fbeccf4cdc fix query outgoing sapronak 2026-01-17 11:01:08 +07:00
Adnan Zahir f2ae2cc731 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!204
2026-01-17 09:04:30 +07:00
Hafizh A. Y. eda50930e7 Merge branch 'Feat/BE/US-Closing_finance' into 'development'
[FEAT][BE]: Refactor closing services and add ClosingKeuanganService and add closing keuangan perkandang(the calculation not accurate yet)

See merge request mbugroup/lti-api!203
2026-01-17 01:21:13 +00:00
Hafizh A. Y. 0bc5480a1d Merge branch 'fix/production-data-dev' into 'development'
[FIX][BE]: fix api production result

See merge request mbugroup/lti-api!202
2026-01-17 01:20:31 +00:00
aguhh18 ef482dd1b9 feat[BE]: Add new ClosingKeuangan DTO and related mapper functions 2026-01-16 21:37:51 +07:00
aguhh18 302f0ed877 fix:[BE] Remove unnecessary filters and update profit loss calculation logic in ClosingKeuanganService 2026-01-16 21:34:49 +07:00
aguhh18 31c48ee1da feat[BE]: Add GetClosingKeuanganByKandang endpoint and related service methods 2026-01-16 20:53:47 +07:00
aguhh18 8ad11af9c9 feat: Refactor closing services and add ClosingKeuanganService
- Updated ClosingRoutes to include ClosingKeuanganService.
- Removed GetClosingKeuangan method from ClosingService interface and its implementation.
- Introduced new ClosingKeuanganService with GetClosingKeuangan method to handle financial logic.
- Implemented detailed logging and error handling in the new service.
- Added GetTotalWeightProducedFromUniformityByProjectFlockID method in RecordingRepository to support weight calculations.
- Enhanced the logic for fetching and classifying product usage data by flags.
- Built comprehensive DTO responses for HPP and Profit Loss sections.
2026-01-16 12:27:18 +07:00
giovanni 688d3fa757 fix api production result 2026-01-15 19:16:58 +07:00
Hafizh A. Y. 409652c15e Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!201
2026-01-15 11:55:47 +00:00
Hafizh A. Y. 08be60c229 Merge branch 'fix/warehouse-provided-location' into 'development'
fix(BE): warehouse provided location

See merge request mbugroup/lti-api!200
2026-01-15 11:53:20 +00:00
Hafizh A. Y. 50b19dc1c3 Merge branch 'fix/BE/Purchase-bop' into 'development'
[FIX/BE-US] adjustment purchase,closing hpp expedition,supplier filter flags

See merge request mbugroup/lti-api!199
2026-01-15 11:53:12 +00:00
Hafizh A. Y e770526c1a fix(BE): warehouse provided location 2026-01-15 18:51:32 +07:00
ragilap 77af262662 [FIX/BE-US] adjustment purchase,closing hpp expedition,supplier filter flags 2026-01-15 18:45:52 +07:00
Adnan Zahir 9d611b7492 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!198
2026-01-15 18:21:47 +07:00
Hafizh A. Y. 21ff1c8ab7 Merge branch 'fix/closing-sapronak-data-produksi' into 'development'
[FIX][BE]: api closing sapronak and data produksi

See merge request mbugroup/lti-api!197
2026-01-15 11:18:49 +00:00
giovanni 2ca84ecffe fixing api closing sapronak and data produksi 2026-01-15 18:16:23 +07:00
Adnan Zahir f13e4f907c Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!196
2026-01-15 17:59:24 +07:00
Hafizh A. Y. 4d334e8d5c Merge branch 'feat/hpp-harian' into 'development'
[FEAT][BE]: add hpp harian

See merge request mbugroup/lti-api!195
2026-01-15 10:54:29 +00:00
Hafizh A. Y. 62522a751f Merge branch 'FEAT/BE/report_customer_payment' into 'development'
[Feat][BE]: creating report customer payment API

See merge request mbugroup/lti-api!190
2026-01-15 10:53:58 +00:00
giovanni 62ccc2e5d6 adjust avg weight 2026-01-15 16:48:37 +07:00
aguhh18 13fc246f21 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment 2026-01-15 16:20:37 +07:00
giovanni 89293a843e adjust api hpp kandang 2026-01-15 16:07:45 +07:00
Adnan Zahir a5af469865 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!192
2026-01-15 15:58:37 +07:00
Hafizh A. Y. 8efe9b668b Merge branch 'fix/nonstock-supplier' into 'development'
fix(BE): remove supplier price in master nonstock

See merge request mbugroup/lti-api!194
2026-01-15 08:52:11 +00:00
Hafizh A. Y. 89480deeb0 Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
[FIX/BE-US] add response warehouse and project flock kandang

See merge request mbugroup/lti-api!193
2026-01-15 08:51:57 +00:00
Hafizh A. Y. 4146342120 Merge branch 'feat/daily-checklist-permission' into 'development'
[FEAT][BE]: add daily checklist permission

See merge request mbugroup/lti-api!191
2026-01-15 08:51:32 +00:00
Hafizh A. Y. b375fb964e Merge branch 'feat/production-result' into 'development'
[FEAT][BE]: adjust api production-result

See merge request mbugroup/lti-api!188
2026-01-15 08:50:33 +00:00
Hafizh A. Y fe002c9602 fix(BE): remove supplier price in master nonstock 2026-01-15 15:49:15 +07:00
ragilap f1032b44d1 [FIX/BE-US] adjustment recording 2026-01-15 15:42:57 +07:00
ragilap 7f2401311b [FIX/BE-US] add response warehouse and project flock kandang 2026-01-15 13:48:00 +07:00
kris 3f4d6c630a Update .gitlab-ci.yml file 2026-01-15 06:46:07 +00:00
aguhh18 8792161c02 feat[BE]: rename Price field to UnitPrice in CustomerPaymentReportRow for clarity 2026-01-15 10:58:00 +07:00
giovanni 37c26d5877 add daily checklist permission 2026-01-15 10:45:13 +07:00
aguhh18 c316a6d7a9 feat[BE]: add address field to CustomerRelationDTO and refactor payment report functions for improved clarity and structure 2026-01-15 10:41:44 +07:00
aguhh18 3a89e18b16 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment 2026-01-14 20:10:31 +07:00
aguhh18 c6dc94a4e1 feat[BE]: add permission requirement for customer payment report route 2026-01-14 20:06:41 +07:00
aguhh18 aeb5433346 feat[BE]: refine customer payment report structure by removing unused fields and enhancing query logic for better performance 2026-01-14 20:00:44 +07:00
kris cad15bcd78 Update .gitlab-ci.yml file 2026-01-14 09:46:39 +00:00
giovanni e004354420 adjust api production-result 2026-01-14 16:20:59 +07:00
kris e0ff6e6d79 Update .gitlab-ci.yml file 2026-01-14 08:26:07 +00:00
aguhh18 804ff45dbd feat[BE]: enhance customer payment report with vehicle numbers and pickup info, add date filtering 2026-01-14 15:15:29 +07:00
kris 9f1c153841 Merge branch 'development' into 'staging'
Edit .gitignore (Tom haye)

See merge request mbugroup/lti-api!184
2026-01-14 07:37:47 +00:00
kris 2a884a8d09 Edit .gitignore (Tom haye) 2026-01-14 07:35:57 +00:00
kris dbf72c7248 Delete .air.toml 2026-01-14 07:06:37 +00:00
aguhh18 7daa509cd0 feat[BE]: update customer payment report to support multiple customer IDs and nullable aging days 2026-01-14 14:06:34 +07:00
kris 894fa0b22a Update .gitlab-ci.yml file 2026-01-14 06:55:28 +00:00
Adnan Zahir d680196919 Merge branch 'development' into 'staging'
Development

See merge request mbugroup/lti-api!180
2026-01-14 13:48:25 +07:00
Adnan Zahir 9fc2d0556e Merge branch 'chore/test-staging' into 'development'
chore: test staging from development

See merge request mbugroup/lti-api!179
2026-01-14 13:46:38 +07:00
Adnan Zahir c2a89910fb chore: test staging from development 2026-01-14 13:46:15 +07:00
Hafizh A. Y. 1847e5590a Merge branch 'feat/informasi-umum' into 'development'
adjust api informasi umum filter per kandang

See merge request mbugroup/lti-api!177
2026-01-14 06:21:21 +00:00
Hafizh A. Y. 57094f664c Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
Feat/be/us 281 adjustment recording

See merge request mbugroup/lti-api!174
2026-01-14 06:20:57 +00:00
Hafizh A. Y. f8b6e12d16 Merge branch 'dev/teguh' into 'development'
FIX[BE]: fixing error on report marketing dto

See merge request mbugroup/lti-api!178
2026-01-14 06:19:52 +00:00
kris e12c34db13 Update .gitlab-ci.yml file 2026-01-14 06:18:47 +00:00
aguhh18 3012d260ec FIX[BE]: fixing error on report marketing dto 2026-01-14 11:57:56 +07:00
aguhh18 f6e872c0aa feat[BE]: implement customer payment report retrieval with pagination and filtering 2026-01-14 11:46:39 +07:00
MacBook Air M1 8a639f127c adjust api informasi umum filter per kandang 2026-01-14 11:41:47 +07:00
Hafizh A. Y. 2acaa10b60 Merge branch 'fix/air.toml' into 'development'
fix(BE): add .air.toml

See merge request mbugroup/lti-api!176
2026-01-14 04:22:29 +00:00
MacBook Air M1 bd9d41e161 fix(BE): add .air.toml 2026-01-14 11:20:42 +07:00
Hafizh A. Y. 06d8d0b795 Merge branch 'feat/price-product-supplier' into 'development'
feat(BE): price-product-supplier

See merge request mbugroup/lti-api!160
2026-01-14 02:17:42 +00:00
ragilap 1d5e7b6e1a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-281-adjustment_recording 2026-01-14 02:09:57 +07:00
Hafizh A. Y. da190f1b05 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
[Feat/be] Adjustment Counting debt supplier

See merge request mbugroup/lti-api!173
2026-01-13 17:40:18 +00:00
Hafizh A. Y. c8905eb715 Merge branch 'feat/BE/sapronak/data-produksi' into 'development'
Feat/be/sapronak/data produksi

See merge request mbugroup/lti-api!170
2026-01-13 17:39:33 +00:00
aguhh18 7f1d796b65 fix[BE]: correct total price calculation in delivery and sales order services 2026-01-13 22:50:58 +07:00
aguhh18 6c7ff3f415 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into FEAT/BE/report_customer_payment 2026-01-13 20:59:50 +07:00
ragilap 03fbf7f4b7 production_standard 2026-01-13 20:06:37 +07:00
ragilap 7545e9b37d [FIX/BE-US] add ignore for dockerfile.local 2026-01-13 19:57:47 +07:00
ragilap 69f38bf16a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier 2026-01-13 19:55:02 +07:00
ragilap 5730053e04 [FIX/BE-US-390] changes counting debt supplier 2026-01-13 19:54:47 +07:00
MacBook Air M1 b087a703ef resolve conflict to development 2026-01-13 16:47:16 +07:00
Adnan Zahir 00edcb6add Merge branch 'chore/test-container-versioning' into 'development'
chore: test image versioning 2

See merge request mbugroup/lti-api!171
2026-01-13 16:47:12 +07:00
Adnan Zahir de6580d11c chore: test image versioning 2 2026-01-13 16:46:49 +07:00
Hafizh A. Y. dcd6008946 Merge branch 'FEAT/BE/report-penjualan' into 'development'
[FEAT][BE]: add some filter on report penjualan API and fixing some error

See merge request mbugroup/lti-api!167
2026-01-13 09:26:01 +00:00
Hafizh A. Y. 711f58abae Merge branch 'feat/BE/US-390-Dashboard' into 'development'
Feat/be/us 390 dashboard changes calculate kilo units

See merge request mbugroup/lti-api!166
2026-01-13 09:25:49 +00:00
Hafizh A. Y. ffd3c905fe Merge branch 'FEAT/BE/Closing_overhead_perkandang' into 'development'
feat[BE]: enhance GetOverhead functionality with project flock kandang count...

See merge request mbugroup/lti-api!165
2026-01-13 09:25:38 +00:00
Hafizh A. Y. 6e4a8617da Merge branch 'Feat/BE/Closing_Penjualan_perkandang' into 'development'
[FEAT][BE] : create closing penjualan perkandang API

See merge request mbugroup/lti-api!164
2026-01-13 09:25:24 +00:00
Adnan Zahir 8a57d5d675 Merge branch 'chore/test-container-versioning' into 'development'
chore: test image versioning

See merge request mbugroup/lti-api!168
2026-01-13 16:18:45 +07:00
Adnan Zahir 5de81f6315 chore: test image versioning 2026-01-13 16:18:15 +07:00
MacBook Air M1 e50dd096a4 Merge branch 'development' into dev/gio 2026-01-13 16:15:15 +07:00
MacBook Air M1 7551d11888 add filter by kandang id sapronak 2026-01-13 16:14:11 +07:00
Adnan Zahir 7444cfac31 Merge branch 'staging' into 'development'
Staging 13 January 2026

See merge request mbugroup/lti-api!163
2026-01-13 16:10:56 +07:00
aguhh18 1b5b5bc847 feat[BE]: add MarketingType filter to marketing reports and update related validations 2026-01-13 16:10:27 +07:00
ragilap 5d7b613ffc [FIX/BE-US-281] changes calculate fcr egg 2026-01-13 15:39:20 +07:00
ragilap 33e89d65ab [FIX/BE-US-281] changes calculate fcr egg 2026-01-13 15:37:54 +07:00
MacBook Air M1 0f4cc6e379 adjust api closing data produksi 2026-01-13 15:32:43 +07:00
MacBook Air M1 590df26a1f adjust api closing production data 2026-01-13 14:43:37 +07:00
ragilap ce7ce778fd [FIX/BE-US-281] add response validation if weeks not have production standart 2026-01-13 14:39:59 +07:00
ragilap eaa208f733 [FIX/BE-US-281] response recording and add payload record_at only in createOne 2026-01-13 14:11:53 +07:00
aguhh18 b088eebac5 feat[BE]: enhance GetOverhead functionality with project flock kandang count mapping and update related DTOs 2026-01-13 13:36:08 +07:00
aguhh18 3c10866208 feat[BE]: add GetOverheadByProjectFlockKandang endpoint and update related services 2026-01-13 13:20:06 +07:00
kris cfbe431222 Update .gitlab-ci.yml file 2026-01-13 04:43:44 +00:00
aguhh18 f7a392be52 feat[BE]: add GetPenjualanByProjectFlockKandang endpoint and update related services 2026-01-13 11:37:23 +07:00
kris 4c434899aa Update .gitlab-ci.yml file 2026-01-13 04:36:34 +00:00
kris 7fd90f3268 Update .gitlab-ci.yml file 2026-01-13 04:19:00 +00:00
kris d26c2dba3f Update .gitlab-ci.yml file 2026-01-13 04:15:08 +00:00
M1 AIR f8415ea15d Update gitlab 2026-01-13 10:59:51 +07:00
M1 AIR 64fe845128 Update CICD 2026-01-13 10:46:55 +07:00
aguhh18 4bd8319e3b FIX[BE] : fixing typografical error on report marketing 2026-01-13 10:16:28 +07:00
aguhh18 bba2dec8c6 FEAT[BE] :update route 2026-01-13 09:52:25 +07:00
Hafizh A. Y. f7b70d4b14 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
[FIX/BE] adjustment response

See merge request mbugroup/lti-api!162
2026-01-13 01:55:59 +00:00
Hafizh A. Y. 9f28294dc3 Merge branch 'FIX/BE/fix_chickin_can't_recording_because_replenish_isn't_correct_for_one_flag_product' into 'development'
FIX[BE] ; fixing chikin replenish. and other logic

See merge request mbugroup/lti-api!161
2026-01-13 01:55:42 +00:00
ragilap 6a166ceb86 [FIX/BE-US-390] dashboard statistic hpp global and avg seling price not refrence to filter 2026-01-13 00:15:48 +07:00
aguhh18 f0b4fe916c FEAT[BE] ;: inisiate customer payment report route and related DTOs 2026-01-12 20:00:49 +07:00
ragilap f37bf4d22d Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier 2026-01-12 16:37:31 +07:00
ragilap ac5edb36e7 [FIX/BE] adjustment response 2026-01-12 16:28:03 +07:00
Hafizh A. Y 8fab5d7d91 feat(BE): price-product-supplier 2026-01-12 14:54:14 +07:00
Hafizh A. Y. 5ddfb2c745 Merge branch 'Feat/BE/Expense_adjust_approval_flow' into 'development'
[Feat][BE}: expense adjust approval flow get rid approval manager change to head area and add business unit vice president approval

See merge request mbugroup/lti-api!159
2026-01-12 06:39:22 +00:00
Hafizh A. Y. 5cfa4d4a59 Merge branch 'fix/daily-checklist' into 'development'
fix route daily checklist

See merge request mbugroup/lti-api!158
2026-01-12 06:38:51 +00:00
aguhh18 80b2cafd2f FIX[BE] ; fixing chikin replenish. and other logic 2026-01-12 13:29:24 +07:00
MacBook Air M1 b47f26d448 adjust max limit location and kandang 2026-01-12 11:30:57 +07:00
M1 AIR 2f22182605 Merge branch 'staging' of https://gitlab.com/mbugroup/lti-api into staging 2026-01-12 11:16:43 +07:00
M1 AIR e2d352721c Merge from development 2026-01-12 11:15:59 +07:00
M1 AIR 068fe4329e Merge remote-tracking branch 'origin/development' into staging 2026-01-12 11:13:24 +07:00
MacBook Air M1 15be8dcbea fix route daily checklist 2026-01-12 11:09:32 +07:00
Hafizh A. Y. 041e8763ac Merge branch 'fix/not-showed-product-supplier' into 'development'
fix(BE): is visible to true in product service

See merge request mbugroup/lti-api!157
2026-01-12 03:39:59 +00:00
Hafizh A. Y. 644e9911e4 Merge branch 'FIX/BE/Case-sensitive' into 'development'
[FIX/BE] adjust case sensitive search To Incase sensitive

See merge request mbugroup/lti-api!156
2026-01-12 03:39:48 +00:00
Hafizh A. Y. bb04cb53d9 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
feat(BE):Rekapitulasi hutang supplier

See merge request mbugroup/lti-api!155
2026-01-12 03:39:26 +00:00
Hafizh A. Y. 048e607290 Merge branch 'feat/daily-checklist-upload-documents' into 'development'
[FEAT][BE]: add api upload documents daily checklist

See merge request mbugroup/lti-api!154
2026-01-12 03:39:06 +00:00
Hafizh A. Y. 18441eb19f Merge branch 'feat/BE/US-390-Dashboard' into 'development'
[FEAT/BE][US-390 dashboard calculation standart]

See merge request mbugroup/lti-api!152
2026-01-12 03:38:51 +00:00
Hafizh A. Y. 526e14f26e Merge branch 'FIX/BE/Transfer_to_laying' into 'development'
[FIX][BE]: fixing transfer to laying qty doesn't  listed on product warehouse and fixing wrong implementation of fifo stock on laying transfer

See merge request mbugroup/lti-api!151
2026-01-12 03:38:27 +00:00
Hafizh A. Y 539081ce99 fix(BE): is visible to true in product service 2026-01-12 10:37:55 +07:00
MacBook Air M1 d568b87e01 adjust response api summary daily checklist 2026-01-12 10:37:33 +07:00
aguhh18 9515848d8f feat(BE): update approval flow to use head area instead of manager 2026-01-12 10:11:31 +07:00
ragilap c15ff8a211 [FIX/BE] add percent rasio in statistic dashboard 2026-01-12 01:15:31 +07:00
ragilap d1d94357cf [FIX/BE] add clamp maximum value 2026-01-12 00:27:53 +07:00
ragilap 67f5165bfb [FIX/BE] adjust case sensitive search 2026-01-11 23:47:28 +07:00
ragilap 1217f34dcd feat(BE):change standart egg in fcr master data 2026-01-11 22:19:20 +07:00
MacBook Air M1 ae41422776 add api upload documents daily checklist 2026-01-11 22:02:21 +07:00
aguhh18 3978951d8f Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into Feat/BE/Expense_adjust_approval_flow 2026-01-11 21:19:35 +07:00
aguhh18 3422fceec7 feat(BE-ExpenseApproval): add unit vice president approval step and permissions 2026-01-11 20:10:19 +07:00
ragilap 09f1b29359 feat(BE):Rekapitulasi hutang supplier 2026-01-11 19:41:21 +07:00
ragilap 167d18fe87 feat(BE-309): add permission dashboard 2026-01-11 19:26:25 +07:00
ragilap 473f4504ea feat(BE-309): changes COMPARASION TO COMPARISON 2026-01-11 19:09:43 +07:00
ragilap dc7dc0ba47 adjustment meta 2026-01-11 18:54:05 +07:00
ragilap a54129866e feat(BE-390): adjustment calculate dashboard 2026-01-11 18:35:23 +07:00
ragilap d40243be4b Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-390-Dashboard 2026-01-11 18:26:40 +07:00
ragilap 525ff650f2 feat(BE-390): calculation dashboard 2026-01-11 15:40:47 +07:00
aguhh18 c1e9b5a975 FIX[BE]: add laying transfer source and target repositories to transfer laying service 2026-01-11 13:30:19 +07:00
aguhh18 272367d8ef FIX[BE]: fixing transfer to laying and implement correct fifo stock 2026-01-11 12:51:37 +07:00
Hafizh A. Y. d3c7d65bf5 Merge branch 'fix/not-showed-product-supplier' into 'development'
[FIX][BE] Permission in payment route

See merge request mbugroup/lti-api!150
2026-01-11 01:44:41 +00:00
Hafizh A. Y. 944fd860a3 Merge branch 'feat/BE/US-74-76-78-278/adjustment_recording_purchase_project_flock' into 'development'
[FIX][BE-74-76-78-278]:adjustment project flock,recording,purchase getall

See merge request mbugroup/lti-api!149
2026-01-11 01:44:25 +00:00
Hafizh A. Y af79db8726 fix(BE): permission in payment route 2026-01-11 08:41:45 +07:00
ragilap b42ca5e6fb feat(BE-74-76-78-278):delete unused code recording 2026-01-10 21:34:19 +07:00
ragilap 3b2c6f16c3 feat(BE-74-76-78-278):adjustment project flock,recording,purchase getall 2026-01-10 21:22:54 +07:00
Hafizh A. Y. 359e982e76 Merge branch 'fix/not-showed-product-supplier' into 'development'
[FIX][BE] Not showed supplier in master data product

See merge request mbugroup/lti-api!148
2026-01-10 11:41:54 +00:00
Hafizh A. Y bc0bf7fe16 fix(BE): not showed supplier in master data product 2026-01-10 18:25:04 +07:00
Hafizh A. Y. 70a7b1b888 Merge branch 'fix/master-data-internal-server-error' into 'development'
[FIX][BE]: code 500 when delete master data foreign to others table

See merge request mbugroup/lti-api!147
2026-01-09 09:44:21 +00:00
Hafizh A. Y 17d55bd2c0 fix(BE): fix code 500 when delete master data foreign to others table 2026-01-09 16:43:20 +07:00
Hafizh A. Y. 9cc86df1ed Merge branch 'fix/seeder-and-finance' into 'development'
[FIX][BE]: Add party account number in payments

See merge request mbugroup/lti-api!146
2026-01-09 09:06:40 +00:00
Hafizh A. Y b7914e8294 fix(BE): add party account number in payments 2026-01-09 16:03:41 +07:00
kris d33119661a Update .gitlab-ci.yml file 2026-01-09 08:37:55 +00:00
Hafizh A. Y 8a57d439dc unfinish: seeder and fix migration 2026-01-09 14:18:28 +07:00
kris 3d76854273 Update .gitlab-ci.yml file 2026-01-09 04:27:45 +00:00
kris b7a3882f20 Update .gitlab-ci.yml file 2026-01-09 04:19:24 +00:00
M1 AIR 29933a5df9 change cicd 2026-01-09 11:17:49 +07:00
M1 AIR f8aee4be7b penyesuaian flow cicid 2026-01-09 10:58:11 +07:00
Hafizh A. Y. 4ee5bf3628 Merge branch 'feat/BE/US-281-uniformity' into 'development'
Feat/be/us 281 uniformity

See merge request mbugroup/lti-api!145
2026-01-09 03:52:26 +00:00
Hafizh A. Y. 0f06dff761 Merge branch 'dev/teguh' into 'development'
FEAT[BE]: add expense to transfer stock, make  document optional on transfer stock,  fixing closing penjualan bad request

See merge request mbugroup/lti-api!144
2026-01-09 03:51:48 +00:00
Hafizh A. Y. 0629c5ccf6 Merge branch 'dev/daily-checklist' into 'development'
adjust validate patch daily checklist

See merge request mbugroup/lti-api!143
2026-01-09 03:44:27 +00:00
ragilap 43eb1df118 feat(BE-281): fixing duplicate 2026-01-09 10:06:22 +07:00
ragilap 338312edd1 feat(BE-281): unique uniformity weeks 2026-01-09 10:04:31 +07:00
ragilap f7522636e2 feat(BE-281): unique uniformity weeks 2026-01-09 09:27:49 +07:00
aguhh18 b11f03dfda feat(BE): fixing wrong perhitungan biaya 2026-01-09 09:15:53 +07:00
aguhh18 76e65704d7 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-08 22:02:59 +07:00
aguhh18 857a3c284b feat(BE): implement movement number generation and refactor transfer creation logic 2026-01-08 22:00:32 +07:00
aguhh18 5606b9c4a3 FIX(BE): fix closing marketing 500 2026-01-08 20:44:56 +07:00
aguhh18 7af78d04dd feat(BE): add file size validation and improve document indexing for transfer creation 2026-01-08 18:52:12 +07:00
aguhh18 2650e919e7 feat(BE): implement expense tracking for stock transfers and enhance related services 2026-01-08 15:14:06 +07:00
Hafizh A. Y. 7b2d3ae025 Merge branch 'dev/daily-checklist' into 'development'
adjust get all phase activity

See merge request mbugroup/lti-api!142
2026-01-08 07:25:06 +00:00
Hafizh A. Y. 64e8de2344 Merge branch 'feat/BE/US-281-uniformity' into 'development'
feat(BE-281): add types uniformity

See merge request mbugroup/lti-api!141
2026-01-08 06:12:41 +00:00
Hafizh A. Y. 2be9ae36c1 Merge branch 'dev/daily-checklist' into 'development'
Adjust limit for get all employee

See merge request mbugroup/lti-api!140
2026-01-08 06:12:06 +00:00
ragilap 6c08fe23ca feat(BE-281): add types uniformity 2026-01-08 12:40:36 +07:00
Hafizh A. Y. 8a64300ddd Merge branch 'dev/daily-checklist' into 'development'
add master data config checklist

See merge request mbugroup/lti-api!139
2026-01-08 02:18:22 +00:00
Hafizh A. Y. 9164550263 Merge branch 'feat/BE/US-281-uniformity' into 'development'
feat(BE-281): fixing recording error, fixing limit upload uniformity and...

See merge request mbugroup/lti-api!138
2026-01-08 02:17:49 +00:00
ragilap a2d2c4269a feat(BE-281): fixing recording error, fixing limit upload uniformity and purchase, add filter and statistic uniformity 2026-01-07 20:26:27 +07:00
Hafizh A. Y. 90f363bfdb Merge branch 'dev/daily-checklist' into 'development'
add module daily checklist

See merge request mbugroup/lti-api!137
2026-01-07 10:55:49 +00:00
Hafizh A. Y. a7a784970d Merge branch 'dev/teguh' into 'development'
FIX[BE]: fix transfer to laying, fix delete biaya, fix chickin, fix nominal expense, and other

See merge request mbugroup/lti-api!136
2026-01-07 07:25:09 +00:00
aguhh18 18b0663dc6 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-07 14:03:08 +07:00
aguhh18 375e057e7c feat(BE): enhance chickin and transfer laying services with product warehouse validation and stockable support 2026-01-07 14:02:39 +07:00
aguhh18 9336289573 feat(BE): add excluded stockables support in FIFO allocation and fetching methods 2026-01-07 13:54:55 +07:00
aguhh18 76d5b6b69a feat(BE): enhance ProjectFlockKandang structure and approval fetching methods 2026-01-07 13:39:54 +07:00
aguhh18 0a84e427c1 FIX[BE]: fixing bug transfer to laying, delet biaya, nominal expesen e, chickin 2026-01-07 09:27:39 +07:00
M1 AIR cad91957b3 Merge development into staging 2026-01-06 18:58:48 +07:00
Hafizh A. Y. fca2d63c6e Merge branch 'dev/gio' into 'development'
fix init population

See merge request mbugroup/lti-api!135
2026-01-06 11:27:13 +00:00
MacBook Air M1 f5a016b74b adjust init population 2026-01-06 17:23:06 +07:00
Hafizh A. Y. 82a7bada05 Merge branch 'dev/gio' into 'development'
feat[BE]: api production result

See merge request mbugroup/lti-api!131
2026-01-06 10:11:25 +00:00
Hafizh A. Y. c6626cb6f5 Merge branch 'development' into 'dev/gio'
# Conflicts:
#   internal/modules/repports/route.go
2026-01-06 10:09:45 +00:00
Hafizh A. Y. ebfa88e721 Merge branch 'dev/daily-checklist' into 'development'
add module daily checklist and master data employee, phase

See merge request mbugroup/lti-api!134
2026-01-06 10:09:12 +00:00
Hafizh A. Y. 705138795c Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
feat(BE-281): adjustment recording to cascade

See merge request mbugroup/lti-api!133
2026-01-06 10:08:54 +00:00
Hafizh A. Y. 538372a43a Merge branch 'feat/BE/US-281-uniformity' into 'development'
[FIX/BE-281] adjustment sso redirect,adjustment response closing,adjustment uniformity

See merge request mbugroup/lti-api!132
2026-01-06 10:08:24 +00:00
ragilap 7a26ca5fe5 feat(BE-281): adjustment recording to cascade 2026-01-06 17:01:09 +07:00
aguhh18 a08466a28e FIX(BE): update foreign key constraints to use ON DELETE NO ACTION for expense and marketing tables 2026-01-06 12:43:52 +07:00
ragilap 1bdaf63763 feat(BE-281): adjustment sso redirect,adjustment response closing,adjustment uniformity 2026-01-06 12:02:19 +07:00
aguhh18 d8fb427734 feat(BE): add grand total calculation to ExpenseListDTO and update CreateOne method in expense service 2026-01-06 11:12:41 +07:00
aguhh18 c9ebd88e9d Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-06 10:10:37 +07:00
aguhh18 0c6d42070a feat(BE): add items field to TransferDeliveryDTO in ToTransferDetailDTO function 2026-01-06 09:03:39 +07:00
MacBook Air M1 8725d79f8f Merge branch 'feat/BE/Sprint-8' into dev/gio 2026-01-02 12:25:50 +07:00
MacBook Air M1 39909d1c2e first commit api production-result 2026-01-02 11:24:26 +07:00
aguhh18 556540e97f Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 14:04:25 +07:00
aguhh18 e421307965 feat(BE): add validation for product category based on flock category in CreateOne method 2025-12-31 13:29:46 +07:00
aguhh18 1b5437bc01 FIX[BE]Lock chickins, accumulate pending qty, use qty key 2025-12-31 13:09:13 +07:00
aguhh18 7d6573fabd FIC[BE]Use qty column for warehouse updates 2025-12-31 13:05:54 +07:00
aguhh18 ce083bccdc feat(BE): add project flock ID to product warehouse creation for approval process 2025-12-31 12:59:17 +07:00
aguhh18 dc4729c3b9 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 11:44:02 +07:00
aguhh18 bec6a93152 FIX[BE]Stop adjusting target product warehouse quantity
Avoid double counting: population entries and ConsumeChickinStocks already update inventory. Pullet/layer for the flock will
be added via other flows (purchase, transfer, etc.)
2025-12-31 11:43:03 +07:00
aguhh18 42853aaac0 fix[BE]Reset chickin pending usage and skip zero entries 2025-12-31 11:34:57 +07:00
aguhh18 610555c3cf Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 10:29:32 +07:00
aguhh18 c60c40af03 feat(be): add GetByProjectFlockKandangIDForUpdate method to lock chickin rows and prevent race conditions 2025-12-31 10:27:15 +07:00
Hafizh A. Y. 2d098cb6b1 Merge branch 'feat/BE/US-281-uniformity' into 'feat/BE/Sprint-8'
feat(BE-281): uniformity and create project flock triger... unfinished s3 read

See merge request mbugroup/lti-api!121
2025-12-31 02:20:01 +00:00
kris 30231fabe9 Edit Dockerfile 2025-12-18 06:51:52 +00:00
kris e738a97e4c Delete docker-compose.yaml 2025-12-18 06:50:41 +00:00
kris 81f4a5e33e Delete docker-compose.local.yml 2025-12-18 06:50:22 +00:00
kris 1e9fdd2b0d Update .gitlab-ci.yml file 2025-12-18 06:41:04 +00:00
GitLab Deploy Bot b6a60d5009 remove 2025-12-15 09:25:50 +07:00
252 changed files with 15731 additions and 4273 deletions
Vendored
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -9,11 +9,13 @@ main
bin/ bin/
*.exe *.exe
*.out *.out
.air.toml
Makefile Makefile
docker-compose.local.yml docker-compose.local.yml
docker-compose.yaml docker-compose.yaml
Dockerfile
Dockerfile.local Dockerfile.local
.gitlab-ci.yml
# Go build cache # Go build cache
.gocache/ .gocache/
vendor vendor
@@ -27,3 +29,4 @@ coverage/
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
.DS_Store
+18 -87
View File
@@ -1,90 +1,21 @@
stages: workflow:
- deploy rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "development"'
- if: '$CI_COMMIT_BRANCH == "staging"'
- if: '$CI_COMMIT_BRANCH == "production"'
- when: never
deploy-dev: include:
stage: deploy - local: "ci/development.yml"
image: alpine:3.20 rules:
variables: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
DEPLOY_APP: "LTI-MBUGROUP" - if: '$CI_COMMIT_BRANCH == "development"'
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
before_script: - local: "ci/staging.yml"
- echo "🧰 Installing dependencies..." rules:
- apk update && apk add --no-cache openssh git curl bash - if: '$CI_COMMIT_BRANCH == "staging"'
# Setup SSH di runner - local: "ci/production.yml"
- mkdir -p ~/.ssh rules:
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa - if: '$CI_COMMIT_BRANCH == "production"'
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
environment:
name: development
+29 -11
View File
@@ -1,20 +1,38 @@
FROM golang:1.23-alpine # =========================
# Builder stage
# =========================
FROM golang:1.23-alpine AS builder
# Install dependensi dasar RUN apk add --no-cache git ca-certificates tzdata
RUN apk add --no-cache git curl bash build-base WORKDIR /app
# Install Air (pakai repo baru air-verse)
RUN go install github.com/air-verse/air@v1.52.3
WORKDIR /lti-api
# Cache dependencies
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
# Copy source code
COPY . . COPY . .
# Build API binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
# Build SEED binary (pastikan cmd/seed ada)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
# =========================
# Runtime stage
# =========================
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
&& adduser -D -H -u 10001 appuser
WORKDIR /app
COPY --from=builder /app/lti-api /app/lti-api
COPY --from=builder /app/lti-seed /app/lti-seed
USER appuser
EXPOSE 8081 EXPOSE 8081
CMD ["air", "-c", ".air.toml"] CMD ["/app/lti-api"]
+1 -1
View File
@@ -110,4 +110,4 @@ IT Development PT Mitra Berlian Unggas Group
## 📃 License ## 📃 License
This project is private. All rights reserved. > This project is private. All rights reserved.
+90
View File
@@ -0,0 +1,90 @@
stages:
- deploy
deploy-dev:
stage: deploy
image: alpine:3.20
variables:
DEPLOY_APP: "LTI-MBUGROUP"
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
before_script:
- echo "🧰 Installing dependencies..."
- apk update && apk add --no-cache openssh git curl bash
# Setup SSH di runner
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
environment:
name: development
+131
View File
@@ -0,0 +1,131 @@
stages:
- build
# - migrate
- deploy
- seed
default:
tags:
- self-hosted-prod
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
when: always
- when: never
variables:
DOCKER_BUILDKIT: "1"
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest"
DEPLOY_DIR: "/opt/deploy/lti"
COMPOSE_FILE: "docker-compose.yaml"
# =========================
# BUILD (AUTO)
# =========================
build_production:
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
echo "✅ Build image: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" -f Dockerfile .
echo "✅ Push image: $IMAGE_NAME"
docker push "$IMAGE_NAME"
echo "✅ Tag latest: $IMAGE_LATEST"
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
docker push "$IMAGE_LATEST"
# =========================
# MIGRATE (PRODUCTION - MANUAL)
# =========================
#migrate_production:
# stage: migrate
# rules:
# - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
# when: manual
# allow_failure: false
# needs:
# - job: build_production
# artifacts: false
# script: |
# set -e
# cd /opt/deploy/lti
# test -f .env || (echo "❌ .env not found" && exit 1)
# set -a
# . ./.env
# set +a
# Validasi env wajib
# : "${DB_HOST:?DB_HOST not set}"
# : "${DB_PORT:?DB_PORT not set}"
# : "${DB_USER:?DB_USER not set}"
# : "${DB_PASSWORD:?DB_PASSWORD not set}"
# : "${DB_NAME:?DB_NAME not set}"
# DB_SSLMODE="${DB_SSLMODE:-require}"
# export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}"
# echo "✅ Running migrations (production)..."
# docker run --rm \
# -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
# migrate/migrate:v4.15.2 \
# -path=/migrations -database "$DATABASE_URL" up
# =========================
# DEPLOY (AUTO)
# =========================
deploy_production:
stage: deploy
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
needs:
# - job: migrate_production
# artifacts: false
- job: build_production
artifacts: false
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
docker compose -f "$COMPOSE_FILE" pull
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
docker image prune -f
# =========================
# SEED (MANUAL)
# =========================
seed_production:
stage: seed
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
when: manual
script: |
set -e
cd /opt/deploy/lti
test -f .env || (echo "❌ .env not found" && exit 1)
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
docker compose --env-file .env pull seed
docker compose --env-file .env run --rm seed
+173
View File
@@ -0,0 +1,173 @@
stages:
- build
- migrate
- deploy
- seed
default:
tags:
- self-hosted-stg
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
when: always
- when: never
variables:
DOCKER_BUILDKIT: "1"
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
COMPOSE_FILE: "docker-compose.yaml"
# =========================
# BUILD (AUTO)
# =========================
build_staging:
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
echo "✅ Build image: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" -f Dockerfile .
echo "✅ Push image: $IMAGE_NAME"
docker push "$IMAGE_NAME"
echo "✅ Tag latest: $IMAGE_LATEST"
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
docker push "$IMAGE_LATEST"
# =========================
# MIGRATE (AUTO)
# =========================
migrate_staging:
stage: migrate
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: build_staging
artifacts: false
script: |
set -e
echo "✅ Running migrations (staging) ..."
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
# ✅ load env dari server
set -a
. ./.env
set +a
# ✅ validasi
test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
echo "✅ DATABASE_URL=$DATABASE_URL"
# ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
echo "✅ Ensuring postgres & redis running ..."
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
# ✅ Ambil network key dari compose
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
# ✅ Cari network name yang dipakai docker
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
echo "✅ Docker network detected: $NETWORK_NAME"
# ✅ Migrations dari repo (CI workspace)
echo "✅ Checking migrations from repo..."
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
echo "✅ Running migrations via migrate/migrate container"
set +e
out=$(docker run --rm \
--network "$NETWORK_NAME" \
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
migrate/migrate:v4.15.2 \
-path=/migrations -database "$DATABASE_URL" up 2>&1)
code=$?
set -e
echo "$out"
# ✅ Handle no change dengan benar (tidak false-success)
if echo "$out" | grep -qi "no change"; then
echo "✅ No change (already up to date)"
exit 0
fi
if [ $code -ne 0 ]; then
echo "❌ Migration failed with exit code $code"
exit $code
fi
echo "✅ Migration applied successfully"
# =========================
# DEPLOY (AUTO)
# =========================
deploy_staging:
stage: deploy
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: migrate_staging
artifacts: false
- job: build_staging
artifacts: false
script: |
set -e
docker info
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
docker compose -f "$COMPOSE_FILE" pull
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
docker image prune -f
# =========================
# SEED (MANUAL)
# =========================
seed_staging:
stage: seed
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs:
- job: deploy_staging
artifacts: false
when: manual
allow_failure: false
script: |
set -e
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
test -f .env || (echo "❌ .env not found" && exit 1)
docker compose -f "$COMPOSE_FILE" pull seed || true
docker compose -f "$COMPOSE_FILE" run --rm seed%
-77
View File
@@ -1,77 +0,0 @@
services:
postgresdb:
image: postgres:alpine
restart: always
ports:
- "${DB_PORT_HOST:-5542}:5432"
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
volumes:
- dbdata:/var/lib/postgresql/data
- ./internal/database/init:/docker-entrypoint-initdb.d
networks: [go-network]
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "${REDIS_PORT_HOST:-6381}:6379"
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 3s
retries: 10
networks: [go-network]
app:
build:
context: .
dockerfile: Dockerfile.local
image: cosmtrek/air:v1.52.3
working_dir: /lti-api
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
command: air -c .air.toml
env_file:
- .env
environment:
DB_HOST: postgresdb
DB_PORT: 5432
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_NAME: ${DB_NAME:-db_lti_erp}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
ports:
- "${APP_PORT:-8081}:8081"
depends_on:
postgresdb:
condition: service_healthy
networks: [go-network]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
volumes:
dbdata:
go-mod-cache:
go-build-cache:
networks:
go-network:
name: lti-api_go-network
driver: bridge
-98
View File
@@ -1,98 +0,0 @@
services:
dev-api-lti:
build:
context: .
dockerfile: Dockerfile
container_name: dev-api-lti
working_dir: /lti-api
command: ["/bin/sh", "scripts/entrypoint.sh"]
ports:
- "8081:8081"
env_file:
- .env
environment:
# override agar koneksi ke container internal
DB_HOST: dev-postgres-lti
DB_PORT: 5432
REDIS_URL: redis://dev-redis-lti:6379/0
volumes:
- .:/lti-api
- ./.air.toml:/lti-api/.air.toml:ro
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-postgres-lti
- dev-redis-lti
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 512M
dev-postgres-lti:
image: postgres:15-alpine
container_name: dev-postgres-lti
restart: always
env_file:
- credential/.env.db
ports:
- "5433:5432"
volumes:
- dev-postgres-lti-data:/var/lib/postgresql/data
- ./credential:/docker-entrypoint-initdb.d:ro
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
dev-redis-lti:
image: redis:7-alpine
container_name: dev-redis-lti
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.2"
memory: 256M
networks:
lti-network:
driver: bridge
volumes:
dev-postgres-lti-data:
+1 -1
View File
@@ -16,6 +16,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.1 github.com/jackc/pgconn v1.14.1
github.com/jackc/pgx/v5 v5.5.5
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
@@ -60,7 +61,6 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
-4
View File
@@ -262,14 +262,10 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 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 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= 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 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -0,0 +1,309 @@
package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
type HppCostRepository interface {
GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error)
GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error)
GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error)
GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error)
GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error)
GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, startDate *time.Time, endDate *time.Time) (float64, float64, error)
GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error)
GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error)
}
type HppRepositoryImpl struct {
db *gorm.DB
}
func NewHppCostRepository(db *gorm.DB) HppCostRepository {
return &HppRepositoryImpl{db: db}
}
func (r *HppRepositoryImpl) GetProjectFlockKandangIDs(ctx context.Context, projectFlockId uint) ([]uint, error) {
var ids []uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("id").
Where("project_flock_id = ?", projectFlockId).
Scan(&ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
func (r *HppRepositoryImpl) GetDocCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0)").
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id AND sa.stockable_type = ?", fifo.UsableKeyProjectChickin.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetBudgetCostByProjectFlockId(ctx context.Context, projectFlockId uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_budgets AS pb").
Select("COALESCE(SUM(pb.qty * pb.price), 0)").
Where("pb.project_flock_id = ?", projectFlockId).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetExpedisionCost(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("expense_nonstocks AS en").
Select("COALESCE(SUM(er.qty * er.price), 0)").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Joins("JOIN flags AS f ON f.flagable_id = en.nonstock_id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
Where("en.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Where("f.name = ?", utils.FlagEkspedisi).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetFeedUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
var total float64
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
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 flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Where("f.name = ?", utils.FlagPakan).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
flags := []utils.FlagType{
utils.FlagOVK,
utils.FlagObat,
utils.FlagVitamin,
utils.FlagKimia,
}
var total float64
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
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 flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Where("f.name IN ?", flags).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetTotalPopulation(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select("COALESCE(SUM(pc.usage_qty), 0)").
Where("pc.project_flock_kandang_id IN (?)", projectFlockKandangIDs).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetPulletCost(ctx context.Context, projectFlockKandangId uint) (float64, error) {
stockablePurchase := fifo.StockableKeyPurchaseItems.String()
stockableTransferIn := fifo.StockableKeyStockTransferIn.String()
usableProjectChickin := fifo.UsableKeyProjectChickin.String()
var total float64
err := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
COALESCE(SUM(pc.usage_qty * CASE
WHEN sa.stockable_type = ? THEN COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(tpi.price, 0)
ELSE 0
END), 0)`,
stockablePurchase, stockableTransferIn).
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = pc.id", usableProjectChickin).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", stockablePurchase).
Joins("LEFT JOIN stock_allocations AS tsa ON tsa.usable_type = ? AND tsa.usable_id = sa.stockable_id AND sa.stockable_type = ? AND tsa.stockable_type = ?", stockableTransferIn, stockableTransferIn, stockablePurchase).
Joins("LEFT JOIN purchase_items AS tpi ON tpi.id = tsa.stockable_id").
Where("pc.project_flock_kandang_id = ?", projectFlockKandangId).
Scan(&total).Error
if err != nil {
return 0, err
}
return total, nil
}
func (r *HppRepositoryImpl) GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(ctx context.Context, projectFlockKandangIDs []uint, date *time.Time) (float64, float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
var totals struct {
TotalPieces float64
TotalWeightKg float64
}
err := r.db.WithContext(ctx).
Table("recordings AS r").
Select("COALESCE(SUM(re.qty), 0) AS total_pieces, COALESCE(SUM(re.weight), 0)AS total_weight_kg").
Joins("JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *date).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
return totals.TotalPieces, totals.TotalWeightKg, nil
}
func (r *HppRepositoryImpl) GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(
ctx context.Context,
projectFlockKandangIDs []uint,
startDate *time.Time,
endDate *time.Time,
) (float64, float64, error) {
if endDate == nil {
now := time.Now()
endDate = &now
}
type subResult struct {
UsableID uint
MdpUsageQty float64
MdpWeight float64
}
subQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
DISTINCT sa.usable_id,
mdp.usage_qty AS mdp_usage_qty,
mdp.total_weight AS mdp_weight
`).
Joins("JOIN recording_eggs re ON re.recording_id = r.id").
Joins(
"JOIN stock_allocations sa ON sa.stockable_type = ? AND sa.stockable_id = re.id AND sa.usable_type = ?",
fifo.StockableKeyRecordingEgg.String(),
fifo.UsableKeyMarketingDelivery.String(),
).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id").
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
Where("r.record_datetime <= ?", *endDate).
Where("mdp.delivery_date = ?", *startDate)
var totals struct {
TotalPieces float64
TotalWeight float64
}
err := r.db.WithContext(ctx).
Table("(?) AS x", subQuery).
Select(`
COALESCE(SUM(x.mdp_usage_qty), 0) AS total_pieces,
COALESCE(SUM(x.mdp_weight), 0) AS total_weight
`).
Scan(&totals).Error
if err != nil {
return 0, 0, err
}
return totals.TotalPieces, totals.TotalWeight, nil
}
func (r *HppRepositoryImpl) GetProjectFlockIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (uint, error) {
var projectFlockID uint
err := r.db.WithContext(ctx).
Table("project_flock_kandangs").
Select("project_flock_id").
Where("id = ?", projectFlockKandangId).
Scan(&projectFlockID).Error
if err != nil {
return 0, err
}
return projectFlockID, nil
}
func (r *HppRepositoryImpl) GetTransferSourceSummary(ctx context.Context, projectFlockKandangId uint) (uint, float64, error) {
var summary struct {
ProjectFlockID uint
TotalQty float64
}
err := r.db.WithContext(ctx).
Table("laying_transfer_targets AS ltt").
Select("lt.from_project_flock_id AS project_flock_id, COALESCE(SUM(ltt.total_qty), 0) AS total_qty").
Joins("JOIN laying_transfers AS lt ON lt.id = ltt.laying_transfer_id").
Where("ltt.target_project_flock_kandang_id = ?", projectFlockKandangId).
Group("lt.from_project_flock_id").
Scan(&summary).Error
if err != nil {
return 0, 0, err
}
return summary.ProjectFlockID, summary.TotalQty, nil
}
+68 -4
View File
@@ -25,6 +25,7 @@ type FifoService interface {
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
} }
type fifoService struct { type fifoService struct {
@@ -95,6 +96,15 @@ type StockReplenishRequest struct {
Tx *gorm.DB Tx *gorm.DB
} }
type StockAdjustRequest struct {
StockableKey fifo.StockableKey
StockableID uint
ProductWarehouseID uint
Quantity float64
Note *string
Tx *gorm.DB
}
type PendingResolution struct { type PendingResolution struct {
UsableKey fifo.UsableKey UsableKey fifo.UsableKey
UsableID uint UsableID uint
@@ -137,6 +147,37 @@ type StockReleaseRequest struct {
Reason *string Reason *string
Tx *gorm.DB Tx *gorm.DB
} }
func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
return errors.New("stockable key and id are required")
}
if req.ProductWarehouseID == 0 {
return errors.New("product warehouse id is required")
}
if req.Quantity == 0 {
return nil
}
if req.Quantity > 0 {
return errors.New("quantity must be negative")
}
cfg, ok := fifo.Stockable(req.StockableKey)
if !ok {
return fmt.Errorf("stockable %q is not registered", req.StockableKey)
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
return err
}
return s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
req.ProductWarehouseID: req.Quantity,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
})
})
}
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) { func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" { if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
@@ -228,7 +269,13 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
switch { switch {
case delta > 0: case delta > 0:
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
var excludedStockables []fifo.StockableKey
if cfg.ExcludedStockables != nil {
excludedStockables = cfg.ExcludedStockables
}
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta, excludedStockables)
if err != nil { if err != nil {
return err return err
} }
@@ -410,8 +457,9 @@ func (s *fifoService) allocateFromStock(
usableKey fifo.UsableKey, usableKey fifo.UsableKey,
usableID uint, usableID uint,
requestQty float64, requestQty float64,
excludedStockables []fifo.StockableKey,
) (*allocationOutcome, error) { ) (*allocationOutcome, error) {
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID) lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -492,14 +540,24 @@ func (s *fifoService) allocateFromStock(
}, nil }, nil
} }
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) { func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint, excludedStockables []fifo.StockableKey) ([]stockLot, error) {
configs := fifo.Stockables() configs := fifo.Stockables()
if len(configs) == 0 { if len(configs) == 0 {
return nil, nil return nil, nil
} }
// Create exclusion set for faster lookup
excludedSet := make(map[fifo.StockableKey]bool)
for _, key := range excludedStockables {
excludedSet[key] = true
}
var lots []stockLot var lots []stockLot
for key, cfg := range configs { for key, cfg := range configs {
// Skip excluded stockables
if excludedSet[key] {
continue
}
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
@@ -616,7 +674,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
continue continue
} }
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending) // Get excluded stockables from candidate usable config
var excludedStockables []fifo.StockableKey
if candidate.Config.ExcludedStockables != nil {
excludedStockables = candidate.Config.ExcludedStockables
}
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending, excludedStockables)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -0,0 +1,272 @@
package service
import (
"context"
"math"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
)
type HppService interface {
CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error)
GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error)
GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error)
GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error)
GetDepresiasiTransfer(projectFlockKandangId uint, date *time.Time) (float64, error)
GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error)
}
type HppCostResponse struct {
Estimation HppCostDetail `json:"estimation"`
Real HppCostDetail `json:"real"`
}
type HppCostDetail struct {
HargaKg float64 `json:"harga_kg"`
HargaButir float64 `json:"harga_butir"`
Total float64 `json:"total"`
Kg float64 `json:"kg"`
Butir float64 `json:"butir"`
}
type hppService struct {
hppRepo commonRepo.HppCostRepository
}
func NewHppService(hppRepo commonRepo.HppCostRepository) HppService {
return &hppService{hppRepo: hppRepo}
}
func (s *hppService) CalculateHppCost(projectFlockKandangId uint, date *time.Time) (*HppCostResponse, error) {
if date == nil {
now := time.Now()
date = &now
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, err
}
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour)
depresiasiTransfer, err := s.GetDepresiasiTransfer(projectFlockKandangId, &endOfDay)
if err != nil {
return nil, err
}
totalProductionCost, err := s.GetTotalProductionCost(projectFlockKandangId, &endOfDay, depresiasiTransfer)
if err != nil {
return nil, err
}
return s.GetHppEstimationDanRealisasi(totalProductionCost, projectFlockKandangId, &startOfDay, &endOfDay)
}
func (s *hppService) GetTotalDepresiasiFlockGrowing(sourceProjectFlockID uint, date *time.Time) (float64, error) {
if date == nil {
now := time.Now()
date = &now
}
if s.hppRepo == nil {
return 0, nil
}
kandangIDs, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
docCost, err := s.hppRepo.GetDocCost(context.Background(), kandangIDs)
if err != nil {
return 0, err
}
budgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
expedisionCost, err := s.hppRepo.GetExpedisionCost(context.Background(), kandangIDs)
if err != nil {
return 0, err
}
feedCost, err := s.hppRepo.GetFeedUsageCost(context.Background(), kandangIDs, date)
if err != nil {
return 0, err
}
ovkCost, err := s.hppRepo.GetOvkUsageCost(context.Background(), kandangIDs, date)
if err != nil {
return 0, err
}
return docCost + budgetCost + expedisionCost + feedCost + ovkCost, nil
}
func (s *hppService) GetTotalProductionCost(projectFlockKandangId uint, endDate *time.Time, depresiasiTransfer float64) (float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
costPullet, err := s.hppRepo.GetPulletCost(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
costFeed, err := s.hppRepo.GetFeedUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
costOvk, err := s.hppRepo.GetOvkUsageCost(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
costExpedision, err := s.hppRepo.GetExpedisionCost(context.Background(), []uint{projectFlockKandangId})
if err != nil {
return 0, err
}
costBudget, err := s.GetBudgetKandangLaying(projectFlockKandangId, endDate)
if err != nil {
return 0, err
}
return depresiasiTransfer + costPullet + costFeed + costOvk + costExpedision + costBudget, nil
}
func (s *hppService) GetBudgetKandangLaying(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
// if date == nil {
// now := time.Now()
// date = &now
// }
if s.hppRepo == nil {
return 0, nil
}
projectFlockId, err := s.hppRepo.GetProjectFlockIDByProjectFlockKandangID(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
projectFlockKandangIds, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), projectFlockId)
if err != nil {
return 0, err
}
eggProduksiPiecesFlock, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), projectFlockKandangIds, endDate)
if err != nil {
return 0, err
}
eggProduksiPiecesKandang, _, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return 0, err
}
totalBudgetCost, err := s.hppRepo.GetBudgetCostByProjectFlockId(context.Background(), projectFlockId)
if err != nil {
return 0, err
}
if eggProduksiPiecesFlock == 0 {
return 0, nil
}
return (totalBudgetCost * eggProduksiPiecesKandang) / eggProduksiPiecesFlock, nil
}
func (s *hppService) GetDepresiasiTransfer(projectFlockKandangId uint, endDate *time.Time) (float64, error) {
// if endDate == nil {
// now := time.Now()
// endDate = &now
// }
if s.hppRepo == nil {
return 0, nil
}
sourceProjectFlockID, transferTotalQty, err := s.hppRepo.GetTransferSourceSummary(context.Background(), projectFlockKandangId)
if err != nil {
return 0, err
}
kandangIDsGrowing, err := s.hppRepo.GetProjectFlockKandangIDs(context.Background(), sourceProjectFlockID)
if err != nil {
return 0, err
}
totalPopulationFlockGrowing, err := s.hppRepo.GetTotalPopulation(context.Background(), kandangIDsGrowing)
if err != nil {
return 0, err
}
if totalPopulationFlockGrowing == 0 {
return 0, nil
}
totalDepresiasiFlockGrowing, err := s.GetTotalDepresiasiFlockGrowing(sourceProjectFlockID, endDate)
if err != nil {
return 0, err
}
return (totalDepresiasiFlockGrowing * transferTotalQty) / totalPopulationFlockGrowing, nil
}
func (s *hppService) GetHppEstimationDanRealisasi(totalProductionCost float64, projectFlockKandangId uint, startDate *time.Time, endDate *time.Time) (*HppCostResponse, error) {
if s.hppRepo == nil {
return &HppCostResponse{}, nil
}
estimPieces, estimWeightKg, err := s.hppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, endDate)
if err != nil {
return nil, err
}
realPieces, realWeightKg, err := s.hppRepo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{projectFlockKandangId}, startDate, endDate)
if err != nil {
return nil, err
}
estimation := HppCostDetail{
Total: totalProductionCost,
Kg: estimWeightKg,
Butir: estimPieces,
}
if estimWeightKg > 0 {
estimation.HargaKg = roundToTwoDecimals(totalProductionCost / estimWeightKg)
}
if estimPieces > 0 {
estimation.HargaButir = roundToTwoDecimals(totalProductionCost / estimPieces)
}
real := HppCostDetail{
Total: totalProductionCost,
Kg: realWeightKg,
Butir: realPieces,
}
if realWeightKg > 0 {
real.HargaKg = roundToTwoDecimals(totalProductionCost / realWeightKg)
}
if realPieces > 0 {
real.HargaButir = roundToTwoDecimals(totalProductionCost / realPieces)
}
return &HppCostResponse{
Estimation: estimation,
Real: real,
}, nil
}
func roundToTwoDecimals(value float64) float64 {
return math.Round(value*100) / 100
}
+2
View File
@@ -54,6 +54,7 @@ var (
SSOAuthorizeURL string SSOAuthorizeURL string
SSOTokenURL string SSOTokenURL string
SSOGetMeURL string SSOGetMeURL string
SSOPortalURL string
SSOClients map[string]SSOClientConfig SSOClients map[string]SSOClientConfig
SSOAccessCookieName string SSOAccessCookieName string
SSORefreshCookieName string SSORefreshCookieName string
@@ -131,6 +132,7 @@ func init() {
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL") SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
SSOTokenURL = viper.GetString("SSO_TOKEN_URL") SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
SSOGetMeURL = viper.GetString("SSO_GETME_URL") SSOGetMeURL = viper.GetString("SSO_GETME_URL")
SSOPortalURL = strings.TrimSpace(viper.GetString("SSO_PORTAL_URL"))
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access") SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh") SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN") SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
+1
View File
@@ -13,6 +13,7 @@ func FiberConfig() fiber.Config {
CaseSensitive: true, CaseSensitive: true,
ServerHeader: "Fiber", ServerHeader: "Fiber",
AppName: "Fiber API", AppName: "Fiber API",
BodyLimit: 8 * 1024 * 1024,
ErrorHandler: utils.ErrorHandler, ErrorHandler: utils.ErrorHandler,
JSONEncoder: sonic.Marshal, JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal, JSONDecoder: sonic.Unmarshal,
@@ -1 +1,2 @@
DROP TABLE IF EXISTS expenses; DROP SEQUENCE IF EXISTS expenses_ref_seq;
DROP TABLE IF EXISTS expenses;
@@ -1,3 +1,3 @@
-- Drop function and sequence for sales order numbers -- Drop function and sequence for sales order numbers
DROP FUNCTION IF EXISTS generate_so_number();
DROP SEQUENCE IF EXISTS so_number_seq; DROP SEQUENCE IF EXISTS so_number_seq;
DROP FUNCTION IF EXISTS generate_so_number();
@@ -1,6 +1,8 @@
DROP TABLE IF EXISTS daily_checklist_tasks; -- Drop tables in correct order (child tables before parent tables)
DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees; DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
DROP TABLE IF EXISTS daily_checklist_activity_tasks; DROP TABLE IF EXISTS daily_checklist_activity_tasks;
DROP TABLE IF EXISTS daily_checklist_tasks;
DROP TABLE IF EXISTS daily_checklist_phases; DROP TABLE IF EXISTS daily_checklist_phases;
DROP TABLE IF EXISTS daily_checklists; DROP TABLE IF EXISTS daily_checklists;
DROP TABLE IF EXISTS checklists; DROP TABLE IF EXISTS checklists;
@@ -0,0 +1,21 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_recordings_project_flock_kandang'
) THEN
ALTER TABLE recordings
DROP CONSTRAINT fk_recordings_project_flock_kandang;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id)
REFERENCES project_flock_kandangs (id)
ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
@@ -0,0 +1,21 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_recordings_project_flock_kandang'
) THEN
ALTER TABLE recordings
DROP CONSTRAINT fk_recordings_project_flock_kandang;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id)
REFERENCES project_flock_kandangs (id)
ON DELETE CASCADE ON UPDATE CASCADE;
COMMIT;
@@ -0,0 +1,15 @@
-- Revert back to NO ACTION (RESTRICT behavior)
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id)
ON DELETE NO ACTION;
-- Revert expense_realizations FK
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
ON DELETE NO ACTION;
@@ -0,0 +1,16 @@
-- Drop existing FK constraints
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
-- Recreate with ON DELETE CASCADE
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id)
ON DELETE CASCADE;
-- Drop and recreate expense_realizations FK
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
ON DELETE CASCADE;
@@ -0,0 +1,20 @@
-- Revert back to NO ACTION (for rollback safety)
DO $$
BEGIN
ALTER TABLE marketing_products DROP CONSTRAINT IF EXISTS fk_marketing_products_marketing_id;
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
ON DELETE NO ACTION;
END $$;
DO $$
BEGIN
ALTER TABLE marketing_delivery_products DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_marketing_product_id;
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
ON DELETE NO ACTION;
END $$;
@@ -0,0 +1,35 @@
-- Ensure marketing_products FK is CASCADE (it should already be, but let's make sure)
DO $$
BEGIN
-- Drop existing FK if exists
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_marketing_products_marketing_id'
) THEN
ALTER TABLE marketing_products DROP CONSTRAINT fk_marketing_products_marketing_id;
END IF;
-- Recreate with ON DELETE CASCADE
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
ON DELETE CASCADE;
END $$;
-- Ensure marketing_delivery_products FK is CASCADE
DO $$
BEGIN
-- Drop existing FK if exists
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_marketing_delivery_products_marketing_product_id'
) THEN
ALTER TABLE marketing_delivery_products DROP CONSTRAINT fk_marketing_delivery_products_marketing_product_id;
END IF;
-- Recreate with ON DELETE CASCADE
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
ON DELETE CASCADE;
END $$;
@@ -0,0 +1,9 @@
-- Drop foreign key and column
ALTER TABLE laying_transfers
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id;
-- Drop index
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
@@ -0,0 +1,19 @@
-- Add product_warehouse_id to laying_transfers for FIFO support
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT;
-- Add foreign key
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- Add index
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
@@ -0,0 +1,14 @@
-- Rollback: Remove STOCKABLE fields from laying_transfers
-- Drop index
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Drop foreign key constraint
ALTER TABLE laying_transfers
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
-- Drop columns
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
@@ -0,0 +1,30 @@
-- Add STOCKABLE fields to laying_transfers for destination warehouse
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
-- Add columns for STOCKABLE role (destination warehouse)
ALTER TABLE laying_transfers
ADD COLUMN dest_product_warehouse_id BIGINT,
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
-- Add foreign key constraint
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- Add index for performance
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- Add comment for documentation
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role';
@@ -0,0 +1,7 @@
-- Remove chart_data, uniform_date, and related indexes
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_uniform_date;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_unique;
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN IF EXISTS chart_data,
DROP COLUMN IF EXISTS uniform_date;
@@ -0,0 +1,25 @@
-- Add uniform_date (if missing), chart_data, and unique constraint for uniformity records
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN IF NOT EXISTS uniform_date TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS chart_data JSONB;
DO $$
BEGIN
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
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date)
WHERE deleted_at IS NULL;
ELSE
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_uniform_date
ON project_flock_kandang_uniformity (uniform_date);
@@ -0,0 +1,4 @@
-- Remove expense_nonstock_id from stock_transfer_details
ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock;
ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id;
DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id;
@@ -0,0 +1,10 @@
-- Add expense_nonstock_id to stock_transfer_details
-- This allows tracking expedition/transport costs for stock transfers (same as purchase)
ALTER TABLE stock_transfer_details
ADD COLUMN expense_nonstock_id BIGINT,
ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL;
-- Create index for better query performance
CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id);
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
DROP COLUMN IF EXISTS party_account_number;
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
COMMIT;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1,20 @@
-- Revert master data foreign keys to CASCADE delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- Update master data foreign keys to RESTRICT delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,79 @@
-- Rollback: Revert FIFO fields back to laying_transfers from detail tables
-- ============================================================================
-- PART 1: Remove FIFO columns from detail tables
-- ============================================================================
-- Add back old qty column first
ALTER TABLE laying_transfer_sources
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE laying_transfer_targets
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
-- Now drop FIFO columns
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_usage_qty;
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add back FIFO columns to laying_transfers table
-- ============================================================================
-- Add columns back for USABLE role (source warehouse)
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT,
ADD COLUMN pending_usage_qty NUMERIC(15, 3),
ADD COLUMN usage_qty NUMERIC(15, 3);
-- Add columns back for STOCKABLE role (destination warehouse)
ALTER TABLE laying_transfers
ADD COLUMN dest_product_warehouse_id BIGINT,
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- ============================================================================
-- PART 3: Recreate foreign key constraints
-- ============================================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
-- Add source product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
-- Add destination product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- ============================================================================
-- PART 4: Recreate indexes for performance
-- ============================================================================
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- ============================================================================
-- PART 5: Recreate comments for documentation
-- ============================================================================
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for FIFO STOCKABLE role';
@@ -0,0 +1,73 @@
-- Move FIFO fields from laying_transfers to detail tables (sources & targets)
-- This enables proper FIFO integration for transfer laying with multiple sources and targets
-- ============================================================================
-- PART 1: Remove FIFO-related columns from laying_transfers table
-- ============================================================================
-- Drop foreign key constraints first
DO $$
BEGIN
-- Drop source product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_product_warehouse_id;
END IF;
-- Drop destination product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id;
END IF;
END $$;
-- Drop indexes
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Remove columns from laying_transfers
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id,
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS pending_usage_qty,
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_sources
ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role';
COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role';
-- Drop old qty column as it's replaced by usage_qty
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS qty;
-- ============================================================================
-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_targets
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role';
COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role';
-- Drop old qty column as it's replaced by total_qty
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS qty;
@@ -0,0 +1,59 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
END IF;
END $$;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,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)
);
COMMIT;
@@ -0,0 +1,57 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS daily_gain,
DROP COLUMN IF EXISTS avg_daily_gain;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 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
(hen_day IS NULL OR hen_day >= 0) AND
(hen_house IS NULL OR hen_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mass IS NULL OR egg_mass >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
@@ -0,0 +1,6 @@
-- Rollback: remove price from supplier relations
ALTER TABLE product_suppliers
DROP COLUMN IF EXISTS price;
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -0,0 +1,6 @@
-- Migration: add price to supplier relations
ALTER TABLE product_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
ALTER TABLE recording_eggs
DROP COLUMN IF EXISTS total_used,
DROP COLUMN IF EXISTS total_qty;
@@ -0,0 +1,7 @@
ALTER TABLE recording_eggs
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
UPDATE recording_eggs
SET total_qty = qty
WHERE total_qty = 0;
@@ -0,0 +1,3 @@
-- Rollback: add price back to nonstock_suppliers
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
@@ -0,0 +1,3 @@
-- Migration: remove price from nonstock_suppliers
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -0,0 +1,4 @@
-- Rollback: Remove requested_qty column from laying_transfer_sources table
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS requested_qty;
@@ -0,0 +1,9 @@
-- Add requested_qty column to laying_transfer_sources table
-- This field stores the quantity requested by user during create/update
-- Separate from UsageQty (FIFO consumed) and PendingUsageQty (FIFO pending)
ALTER TABLE laying_transfer_sources
ADD COLUMN requested_qty NUMERIC(15,3) DEFAULT 0 NOT NULL;
-- Add comment for documentation
COMMENT ON COLUMN laying_transfer_sources.requested_qty IS 'Quantity requested by user during create/update';
@@ -0,0 +1,3 @@
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS source_product_warehouse_id;
@@ -0,0 +1,17 @@
ALTER TABLE recording_depletions
ADD COLUMN IF NOT EXISTS pending_qty numeric(15,3) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS source_product_warehouse_id bigint;
UPDATE recording_depletions rd
SET source_product_warehouse_id = src.product_warehouse_id
FROM recordings r
JOIN LATERAL (
SELECT pfp.product_warehouse_id
FROM project_chickins pc
JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id
WHERE pc.project_flock_kandang_id = r.project_flock_kandangs_id
ORDER BY pfp.created_at ASC, pfp.id ASC
LIMIT 1
) AS src ON true
WHERE r.id = rd.recording_id
AND rd.source_product_warehouse_id IS NULL;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ALTER COLUMN bank_id SET NOT NULL;
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ALTER COLUMN bank_id DROP NOT NULL;
COMMIT;
@@ -0,0 +1,3 @@
ALTER TABLE adjustment_stocks ADD COLUMN stock_log_id INTEGER;
CREATE INDEX idx_adjustment_stocks_stock_log_id ON adjustment_stocks (stock_log_id);
@@ -0,0 +1 @@
ALTER TABLE adjustment_stocks DROP COLUMN IF EXISTS stock_log_id;
@@ -0,0 +1,2 @@
ALTER TABLE stock_logs
DROP COLUMN stock;
@@ -0,0 +1,19 @@
ALTER TABLE stock_logs
ADD COLUMN stock NUMERIC(15, 3) NOT NULL DEFAULT 0;
WITH calc AS (
SELECT
id,
SUM(COALESCE(increase, 0) - COALESCE(decrease, 0))
OVER (
PARTITION BY product_warehouse_id
ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_stock
FROM stock_logs
)
UPDATE stock_logs t
SET stock = c.running_stock
FROM calc c
WHERE t.id = c.id;
+1
View File
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Tax: tax, Tax: tax,
ExpiryPeriod: seed.Expiry, ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy, CreatedBy: createdBy,
IsVisible: seed.IsVisible,
} }
if err := tx.Create(&product).Error; err != nil { if err := tx.Create(&product).Error; err != nil {
return err return err
+9 -21
View File
@@ -2,28 +2,16 @@ package entities
import "time" 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 { type AdjustmentStock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
StockLogId uint `gorm:"column:stock_log_id;not null;index"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` TotalQty float64 `gorm:"column:total_qty;default:0"`
TotalUsed float64 `gorm:"column:total_used;default:0"`
UsageQty float64 `gorm:"column:usage_qty;default:0"`
PendingQty float64 `gorm:"column:pending_qty;default:0"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
// === 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"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
} }
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Dashboard 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"`
}
+9 -9
View File
@@ -5,15 +5,15 @@ import (
) )
type ExpenseNonstock struct { type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""` ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""` ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""` KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""` NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;column:price"` Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
Notes string `gorm:"type:text;column:notes"` Notes string `gorm:"type:text;column:notes"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
+6 -8
View File
@@ -12,18 +12,16 @@ type LayingTransfer struct {
FromProjectFlockId uint `gorm:"not null"` FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"` TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
Notes string `gorm:"type:text"` Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
+3 -1
View File
@@ -11,7 +11,9 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"` SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` RequestedQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // Quantity requested by user
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -1
View File
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"` TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"` TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+17 -16
View File
@@ -7,22 +7,23 @@ import (
) )
type Payment struct { type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"` PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"` ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"` TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` 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"` PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"` PartyAccountNumber *string `gorm:"type:varchar(50)"`
PaymentMethod string `gorm:"type:varchar(20);not null"` PaymentDate time.Time `gorm:"not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"` PaymentMethod string `gorm:"type:varchar(20);not null"`
Direction string `gorm:"type:varchar(5);not null"` BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Nominal float64 `gorm:"type:numeric(15,3);not null"` Direction string `gorm:"type:varchar(5);not null"`
Notes string `gorm:"type:text;not null"` Nominal float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` Notes string `gorm:"type:text;not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"` BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+7 -6
View File
@@ -7,12 +7,13 @@ import (
) )
type Phases struct { type Phases struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"` IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"` Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ActivityCount int `gorm:"-" json:"-"`
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"` Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
} }
+1 -1
View File
@@ -21,7 +21,7 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsVisible bool `gorm:"column:is_visible;default:true"` IsVisible bool ``
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type ProductSupplier struct { type ProductSupplier struct {
ProductId uint `gorm:"not null"` ProductId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"` SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"` Product Product `gorm:"foreignKey:ProductId;references:Id"`
@@ -1,20 +1,24 @@
package entities package entities
import "time" import (
"encoding/json"
"time"
)
type ProjectFlockKandangUniformity struct { type ProjectFlockKandangUniformity struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Uniformity float64 `gorm:"type:numeric(15,3)"` Uniformity float64 `gorm:"type:numeric(15,3)"`
Week int `gorm:"not null"` Week int `gorm:"not null"`
Cv float64 `gorm:"type:numeric(15,3)"` Cv float64 `gorm:"type:numeric(15,3)"`
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"` ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
MeanUp float64 `gorm:"type:numeric(15,3)"` MeanUp float64 `gorm:"type:numeric(15,3)"`
MeanDown float64 `gorm:"type:numeric(15,3)"` MeanDown float64 `gorm:"type:numeric(15,3)"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null"`
UniformQty float64 `gorm:"type:numeric(15,3)"` UniformQty float64 `gorm:"type:numeric(15,3)"`
NotUniformQty float64 `gorm:"type:numeric(15,3)"` NotUniformQty float64 `gorm:"type:numeric(15,3)"`
UniformDate *time.Time `gorm:"type:timestamptz"` ChartData json.RawMessage `gorm:"type:jsonb"`
CreatedBy uint `gorm:"not null"` UniformDate *time.Time `gorm:"type:timestamptz"`
CreatedBy uint `gorm:"not null"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+5 -4
View File
@@ -10,8 +10,9 @@ type ProjectFlockKandang struct {
ClosedAt *time.Time `gorm:"index"` ClosedAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestProjectFlockApproval *Approval `gorm:"-" json:"-"`
LatestChickinApproval *Approval `gorm:"-" json:"-"`
} }
+6 -6
View File
@@ -16,10 +16,10 @@ type Recording struct {
CumIntake *int `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
HandDay *float64 `gorm:"column:hand_day"` HenDay *float64 `gorm:"column:hen_day"`
HandHouse *float64 `gorm:"column:hand_house"` HenHouse *float64 `gorm:"column:hen_house"`
FeedIntake *float64 `gorm:"column:feed_intake"` FeedIntake *float64 `gorm:"column:feed_intake"`
EggMesh *float64 `gorm:"column:egg_mesh"` EggMass *float64 `gorm:"column:egg_mass"`
EggWeight *float64 `gorm:"column:egg_weight"` EggWeight *float64 `gorm:"column:egg_weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -34,11 +34,11 @@ type Recording struct {
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
StandardHandDay *float64 `gorm:"-"` StandardHenDay *float64 `gorm:"-"`
StandardHandHouse *float64 `gorm:"-"` StandardHenHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"` StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"` StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMesh *float64 `gorm:"-"` StandardEggMass *float64 `gorm:"-"`
StandardEggWeight *float64 `gorm:"-"` StandardEggWeight *float64 `gorm:"-"`
StandardFcr *float64 `gorm:"-"` StandardFcr *float64 `gorm:"-"`
} }
+6 -4
View File
@@ -1,10 +1,12 @@
package entities package entities
type RecordingDepletion struct { type RecordingDepletion struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty float64 `gorm:"column:qty;not null"` SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
Qty float64 `gorm:"column:qty;not null"`
PendingQty float64 `gorm:"column:pending_qty"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
+3
View File
@@ -7,11 +7,14 @@ type RecordingEgg struct {
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"` Qty int `gorm:"column:qty;not null"`
TotalQty float64 `gorm:"column:total_qty"`
TotalUsed float64 `gorm:"column:total_used"`
Weight *float64 `gorm:"column:weight"` Weight *float64 `gorm:"column:weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
} }
+1
View File
@@ -9,6 +9,7 @@ type StockLog struct {
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"` Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"` Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
Stock float64 `gorm:"column:stock;type:numeric(15,3);not null;default:0"`
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"` LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
LoggableId uint `gorm:"column:loggable_id;not null"` LoggableId uint `gorm:"column:loggable_id;not null"`
+8 -13
View File
@@ -8,27 +8,22 @@ type StockTransferDetail struct {
StockTransferId uint64 StockTransferId uint64
ProductId uint64 ProductId uint64
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
// Tracking stock yang DIAMBIL dari source warehouse
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"` SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock) PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) === TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
// Tracking stock yang DITAMBAHKAN ke destination warehouse TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"` ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia CreatedAt time.Time
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === METADATA ===
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === RELATIONS === // === RELATIONS ===
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"` Product *Product `gorm:"foreignKey:ProductId"`
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"` SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"` DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"` DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
} }
+51 -31
View File
@@ -1,5 +1,9 @@
package middleware package middleware
const (
P_DashboardGetAll = "lti.dashboard.list"
)
// project-flock // project-flock
const ( const (
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
@@ -19,18 +23,19 @@ const (
) )
const ( const (
P_ExpenseGetAll = "lti.expense.list" P_ExpenseGetAll = "lti.expense.list"
P_ExpenseCreateOne = "lti.expense.create" P_ExpenseCreateOne = "lti.expense.create"
P_ExpenseUpdateOne = "lti.expense.update" P_ExpenseUpdateOne = "lti.expense.update"
P_ExpenseGetOne = "lti.expense.detail" P_ExpenseGetOne = "lti.expense.detail"
P_ExpenseDeleteOne = "lti.expense.delete" P_ExpenseDeleteOne = "lti.expense.delete"
P_ExpenseApprovalManager = "lti.expense.approve.manager" P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area"
P_ExpenseApprovalFinance = "lti.expense.approve.finance" P_ExpenseApprovalFinance = "lti.expense.approve.finance"
P_ExpenseCreateRealizations = "lti.expense.create.realization" P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
P_ExpenseUpdateRealizations = "lti.expense.update.realization" P_ExpenseCreateRealizations = "lti.expense.create.realization"
P_ExpenseCompleteExpense = "lti.expense.complete.expense" P_ExpenseUpdateRealizations = "lti.expense.update.realization"
P_ExpenseDocument = "lti.expense.document" P_ExpenseCompleteExpense = "lti.expense.complete.expense"
P_ExpenseDocumentRealizations = "lti.expense.document.realization" P_ExpenseDocument = "lti.expense.document"
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
) )
const ( const (
P_AdjustmentGetAll = "lti.inventory.list" P_AdjustmentGetAll = "lti.inventory.list"
@@ -44,7 +49,10 @@ const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list"
) )
const ( const (
@@ -134,17 +142,17 @@ const (
P_NonstocksUpdateOne = "lti.master.nonstocks.update" P_NonstocksUpdateOne = "lti.master.nonstocks.update"
P_NonstocksDeleteOne = "lti.master.nonstocks.delete" P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
P_ProductCategoriesGetAll = "lti.master.Product_categories.list" P_ProductCategoriesGetAll = "lti.master.product_categories.list"
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete"
P_ProductsGetAll = "lti.master.Products.list" P_ProductsGetAll = "lti.master.products.list"
P_ProductsGetOne = "lti.master.Products.detail" P_ProductsGetOne = "lti.master.products.detail"
P_ProductsCreateOne = "lti.master.Products.create" P_ProductsCreateOne = "lti.master.products.create"
P_ProductsUpdateOne = "lti.master.Products.update" P_ProductsUpdateOne = "lti.master.products.update"
P_ProductsDeleteOne = "lti.master.Products.delete" P_ProductsDeleteOne = "lti.master.products.delete"
P_SuppliersGetAll = "lti.master.suppliers.list" P_SuppliersGetAll = "lti.master.suppliers.list"
P_SuppliersGetOne = "lti.master.suppliers.detail" P_SuppliersGetOne = "lti.master.suppliers.detail"
@@ -207,15 +215,15 @@ const (
) )
const ( const (
P_PurchaseGetAll = "lti.Purchase.list" P_PurchaseGetAll = "lti.purchase.list"
P_PurchaseGetOne = "lti.Purchase.detail" P_PurchaseGetOne = "lti.purchase.detail"
P_PurchaseCreateOne = "lti.Purchase.create" P_PurchaseCreateOne = "lti.purchase.create"
P_PurchaseUpdateOne = "lti.Purchase.update" P_PurchaseUpdateOne = "lti.purchase.update"
P_PurchaseDeleteOne = "lti.Purchase.delete" P_PurchaseDeleteOne = "lti.purchase.delete"
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" P_PurchaseItemDeleteOne = "lti.purchase.delete.item"
P_PurchaseReceive = "lti.Purchase.receive" P_PurchaseReceive = "lti.purchase.receive"
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" P_PurchaseApprovalStaff = "lti.purchase.approve.staff"
P_PurchaseApprovalManager = "lti.Purchase.approve.manager" P_PurchaseApprovalManager = "lti.purchase.approve.manager"
) )
const ( const (
@@ -232,3 +240,15 @@ const (
P_UserGetAll = "lti.users.list" P_UserGetAll = "lti.users.list"
P_UserGetOne = "lti.users.detail" P_UserGetOne = "lti.users.detail"
) )
// daily-checklist
const (
P_DailyChecklistDashboardList = "lti.daily_checklist.dashboard.list"
P_DailyChecklistCreateOne = "lti.daily_checklist.create"
P_DailyChecklistGetAll = "lti.daily_checklist.list"
P_DailyChecklistGetOne = "lti.daily_checklist.detail"
P_DailyChecklistReports = "lti.daily_checklist.reports"
P_DailyChecklistEmployee = "lti.daily_checklist.master_data.employee"
P_DailyChecklistActivity = "lti.daily_checklist.master_data.activity"
P_DailyChecklistActivityConfig = "lti.daily_checklist.master_data.configuration"
)
@@ -14,22 +14,45 @@ import (
) )
type ClosingController struct { type ClosingController struct {
ClosingService service.ClosingService ClosingService service.ClosingService
SapronakService service.SapronakService SapronakService service.SapronakService
ClosingKeuanganService service.ClosingKeuanganService
} }
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController { func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, closingKeuanganService service.ClosingKeuanganService) *ClosingController {
return &ClosingController{ return &ClosingController{
ClosingService: closingService, ClosingService: closingService,
SapronakService: sapronakService, SapronakService: sapronakService,
ClosingKeuanganService: closingKeuanganService,
} }
} }
func (u *ClosingController) GetAll(c *fiber.Ctx) error { func (u *ClosingController) GetAll(c *fiber.Ctx) error {
var projectStatus *int
if raw := c.Query("project_status"); raw != "" {
statusValue, err := strconv.Atoi(raw)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_status")
}
projectStatus = &statusValue
}
var locationID *uint
if raw := c.Query("location_id"); raw != "" {
locationValue, err := strconv.Atoi(raw)
if err != nil || locationValue <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
}
locationUint := uint(locationValue)
locationID = &locationUint
}
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
ProjectStatus: projectStatus,
LocationID: locationID,
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -78,6 +101,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetOverheadByProjectFlockKandang(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.GetOverhead(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead by project flock kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error { func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId") param := c.Params("projectFlockId")
@@ -86,7 +139,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
} }
result, err := u.ClosingService.GetClosingSummary(c, uint(id)) var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id), kandangID)
if err != nil { if err != nil {
return err return err
} }
@@ -108,12 +171,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
} }
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID)) result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
if err != nil { if err != nil {
return err return err
} }
@@ -123,19 +181,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get closing penjualan successfully", Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result), Data: dto.ToPenjualanRealisasiResponseDTO(result),
})
}
func (u *ClosingController) GetPenjualanByProjectFlockKandang(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.GetPenjualan(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan by project flock kandang successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(result),
}) })
} }
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error { func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
param := c.Params("project_flock_id") projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(param) projectFlockID, err := strconv.Atoi(projectParam)
if err != nil { if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
} }
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID)) var projectFlockKandangID *uint
if kandangParam != "" {
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
projectFlockKandangID = &kandangID
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
if err != nil { if err != nil {
return err return err
} }
@@ -158,9 +257,18 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
} }
query := &validation.ClosingSapronakQuery{ query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")), Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search"),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -191,6 +299,45 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingSapronakSummary(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")
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Search: c.Query("search"),
}
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
result, err := u.ClosingService.GetClosingSapronakSummary(c, uint(id), query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved closing report (sapronak summary) successfully",
Data: result,
})
}
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error { func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("project_flock_id")
flag := c.Query("flag", "") flag := c.Query("flag", "")
@@ -247,14 +394,14 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
} }
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("projectFlockId")
projectFlockID, err := strconv.Atoi(param) projectFlockID, err := strconv.Atoi(param)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
} }
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID)) result, err := u.ClosingKeuanganService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil { if err != nil {
return err return err
} }
@@ -268,6 +415,34 @@ func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetClosingKeuanganByKandang(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")
}
result, err := u.ClosingKeuanganService.GetClosingKeuanganByKandang(c, uint(projectFlockID), uint(pfkID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan by kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("project_flock_id")
@@ -338,7 +513,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
} }
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID)
if err != nil { if err != nil {
return err return err
} }
+38 -12
View File
@@ -59,39 +59,65 @@ type ClosingSummaryDTO struct {
StatusClosing string `json:"closing_status"` StatusClosing string `json:"closing_status"`
} }
type ClosingSummaryKandangDTO struct {
FlockID uint `json:"flock_id"`
Period int `json:"period"`
LocationName string `json:"location_name"`
Population int `json:"population"`
PopulationFormatted string `json:"population_formatted"`
ProjectType string `json:"project_type"`
ClosingDate string `json:"closing_date"`
KandangName string `json:"kandang_name"`
ChickInDate string `json:"chick_in_date"`
PicName string `json:"pic_name"`
ApprovalDate string `json:"approval_date"`
ProjectStatus string `json:"project_status"`
}
type ClosingPurchaseDTO struct { type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"` InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"` ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"` FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"` FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"` FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"` // FeedUsedPerHead float64 `json:"feed_used_per_head"`
} }
type ClosingSalesDTO struct { type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"` SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"` SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"` AverageWeight float64 `json:"avg_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingEggSalesDTO struct { type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"` EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"` EggMassKg float64 `json:"egg_mass"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"` AverageEggWeightKg float64 `json:"avg_egg_weight"`
AverageSellingPrice float64 `json:"egg_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingPerformanceDTO struct { type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"` Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"` Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"` MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mortality_act"` MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"deff_mortality"` DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"` FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"` FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"` DeffFcr float64 `json:"fcr_diff"`
Awg float64 `json:"awg"` AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"`
EggMass float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeight float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"`
} }
type ClosingSalesGroupDTO struct { type ClosingSalesGroupDTO struct {
@@ -164,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64 var total float64
for _, h := range history { for _, h := range history {
for _, chickin := range h.Chickins { for _, chickin := range h.Chickins {
total += chickin.UsageQty + chickin.PendingUsageQty total += chickin.UsageQty
} }
} }
return total return total
@@ -1,59 +1,31 @@
package dto package dto
import ( import (
"slices"
"strings" "strings"
"gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === CONSTANTS === type ClosingHPPCode string
const ( const (
HPPGroupPengeluaran = "HPP dan Pengeluaran" HPPCodePakan ClosingHPPCode = "PAKAN"
HPPGroupBahanBaku = "HPP dan Bahan Baku" HPPCodeOVK ClosingHPPCode = "OVK"
HPPLabelOverhead = "Pengeluaran Overhead" HPPCodeDOC ClosingHPPCode = "DOC"
HPPLabelEkspedisi = "Beban Ekspedisi" HPPCodeDepresiasi ClosingHPPCode = "DEPRESIASI"
HPPSummaryLabel = "HPP" HPPCodeOverhead ClosingHPPCode = "OVERHEAD"
HPPCodeEkspedisi ClosingHPPCode = "EKSPEDISI"
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 ClosingProfitLossCode string
type CalculationContext struct { const (
TotalPopulation float64 PLCodeSales ClosingProfitLossCode = "SALES"
TotalWeightProduced float64 PLCodeSapronak ClosingProfitLossCode = "SAPRONAK"
TotalEggWeightKg float64 PLCodeOverhead ClosingProfitLossCode = "OVERHEAD"
TotalDepletion float64 PLCodeEkspedisi ClosingProfitLossCode = "EKSPEDISI"
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 { type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"` RpPerBird float64 `json:"rp_per_bird"`
@@ -61,73 +33,57 @@ type FinancialMetrics struct {
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
} }
type Comparison struct { type HPPItem struct {
ID uint `json:"id"`
Category string `json:"category"`
Code string `json:"code"`
Label string `json:"label"`
Budgeting FinancialMetrics `json:"budgeting"` Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"` Realization FinancialMetrics `json:"realization"`
} }
// === HPP PURCHASES PACKAGE === type HPPSummary struct {
Label string `json:"label"`
type HppItem struct { Budgeting FinancialMetrics `json:"budgeting"`
Type string `json:"type"` Realization FinancialMetrics `json:"realization"`
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"` EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
} }
type HppPurchasesSection struct { type HPPSection struct {
Hpp []HppGroup `json:"hpp"` Items []HPPItem `json:"items"`
SummaryHpp SummaryHpp `json:"summary_hpp"` Summary HPPSummary `json:"summary"`
} }
// === PROFIT LOSS PACKAGE === type ProfitLossItem struct {
Code string `json:"code"`
type PLItem struct { Label string `json:"label"`
Type string `json:"type"` Type string `json:"type"`
FinancialMetrics RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
} }
type PLSummaryItem struct { type ProfitLossSummary struct {
Label string `json:"label"` GrossProfit FinancialMetrics `json:"gross_profit"`
FinancialMetrics SubTotal FinancialMetrics `json:"sub_total"`
} NetProfit FinancialMetrics `json:"net_profit"`
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 { type ProfitLossSection struct {
Data ProfitLossData `json:"data"` Items []ProfitLossItem `json:"items"`
Summary ProfitLossSummary `json:"summary"`
} }
// === RESPONSE DTO (ROOT) === type ClosingKeuanganData struct {
HPP HPPSection `json:"hpp"`
type ReportResponse struct { ProfitLoss ProfitLossSection `json:"profit_loss"`
HppPurchases HppPurchasesSection `json:"hpp_purchases"` }
ProfitLoss ProfitLossSection `json:"profit_loss"` type MetricsCalculator struct {
TotalPopulation float64
ActualPopulation float64
TotalWeightProduced float64
} }
// === MAPPER FUNCTIONS ===
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{ return FinancialMetrics{
@@ -137,453 +93,133 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
} }
} }
func ToComparison(budgeting, realization FinancialMetrics) Comparison { func ToHPPItem(id uint, category, code, label string, budgeting, realization FinancialMetrics) HPPItem {
return Comparison{ return HPPItem{
ID: id,
Category: category,
Code: code,
Label: label,
Budgeting: budgeting, Budgeting: budgeting,
Realization: realization, Realization: realization,
} }
} }
// === HPP PENGELUARAN (from Purchase Items) === func ToHPPSummary(label string, budgeting, realization FinancialMetrics, eggBudgeting, eggRealization *FinancialMetrics) HPPSummary {
return HPPSummary{
func getFlagLabel(flagType utils.FlagType) string { Label: label,
return PurchaseLabelPrefix + string(flagType) Budgeting: budgeting,
} Realization: realization,
EggBudgeting: eggBudgeting,
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem { EggRealization: eggRealization,
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 { func ToHPPSection(items []HPPItem, summary HPPSummary) HPPSection {
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) return HPPSection{
Items: items,
return HppItem{ Summary: summary,
Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
} }
} }
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup { func ToProfitLossItem(code, label, itemType string, rpPerBird, rpPerKg, amount float64) ProfitLossItem {
items := []HppItem{} return ProfitLossItem{
Code: code,
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) Label: label,
realizationAmount := getOperationalExpenses(realizations) Type: itemType,
RpPerBird: rpPerBird,
if budgetAmount > 0 || realizationAmount > 0 { RpPerKg: rpPerKg,
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx)) Amount: amount,
}
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
} }
} }
// === HPP SUMMARY === func ToProfitLossSummary(grossProfit, subTotal, netProfit FinancialMetrics) ProfitLossSummary {
return ProfitLossSummary{
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp { GrossProfit: grossProfit,
purchaseTotal := sumPurchaseTotal(purchaseItems) SubTotal: subTotal,
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) NetProfit: netProfit,
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 ToProfitLossSection(items []ProfitLossItem, summary ProfitLossSummary) ProfitLossSection {
return ProfitLossSection{
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { Items: items,
return PLItem{ Summary: summary,
Type: itemType,
FinancialMetrics: metrics,
} }
} }
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { func ToClosingKeuanganData(hpp HPPSection, profitLoss ProfitLossSection) ClosingKeuanganData {
return PLSummaryItem{ return ClosingKeuanganData{
Label: label, HPP: hpp,
FinancialMetrics: metrics, ProfitLoss: profitLoss,
} }
} }
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem { func (mc *MetricsCalculator) CalculateMetrics(amount float64) (rpPerBird, rpPerKg float64) {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced) if mc.ActualPopulation > 0 {
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) rpPerBird = amount / mc.ActualPopulation
} }
if mc.TotalWeightProduced > 0 {
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { rpPerKg = amount / mc.TotalWeightProduced
for _, item := range items {
totalAmount += item.Amount
totalPerBird += item.RpPerBird
} }
return return
} }
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem { func (mc *MetricsCalculator) CalculateProfitLossMetrics(amount float64) (rpPerBird, rpPerKg float64) {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold) if mc.TotalPopulation > 0 {
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) rpPerBird = amount / mc.TotalPopulation
}
if mc.TotalWeightProduced > 0 {
rpPerKg = amount / mc.TotalWeightProduced
}
return
} }
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem { type ProductFilter struct {
items := []PLItem{} ProjectFlockCategory string
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 { func (pf *ProductFilter) IsEggProduct(product entity.Product) bool {
purchaseAmount := sumPurchaseTotal(purchases) for _, flag := range product.Flags {
flagName := strings.ToUpper(flag.Name)
return []PLItem{ if flagName == string(utils.FlagTelur) ||
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx), flagName == string(utils.FlagTelurUtuh) ||
} flagName == string(utils.FlagTelurPecah) ||
} flagName == string(utils.FlagTelurPutih) ||
flagName == string(utils.FlagTelurRetak) {
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 true
} }
} }
return false return false
} }
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { func (pf *ProductFilter) IsChickenProduct(product entity.Product) 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 { for _, flag := range product.Flags {
flagType := utils.FlagType(strings.ToUpper(flag.Name)) flagName := strings.ToUpper(flag.Name)
if flagName == string(utils.FlagAyamAfkir) ||
if isEggProductFlag(flagType) { flagName == string(utils.FlagAyamCulling) ||
return PLSalesTypeEgg flagName == string(utils.FlagAyamMati) {
} return true
if isChickenProductFlag(flagType) {
return PLSalesTypeChicken
} }
} }
return false
return PLSalesTypeChicken
} }
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { func (pf *ProductFilter) ShouldIncludeProduct(product entity.Product) bool {
categorized := make(map[string][]entities.MarketingDeliveryProduct) if pf.ProjectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
return pf.IsEggProduct(product)
for _, delivery := range deliveries {
product := delivery.MarketingProduct.ProductWarehouse.Product
salesType := getSalesTypeFromProductFlags(&product)
categorized[salesType] = append(categorized[salesType], delivery)
} }
return pf.IsChickenProduct(product) || (!pf.IsEggProduct(product) && !pf.IsChickenProduct(product))
return categorized
} }
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 { func (pf *ProductFilter) FilterDeliveryProducts(deliveries []entity.MarketingDeliveryProduct) []entity.MarketingDeliveryProduct {
amount := 0.0 filtered := make([]entity.MarketingDeliveryProduct, 0)
for _, delivery := range deliveries { for _, delivery := range deliveries {
amount += delivery.TotalPrice if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
if pf.ShouldIncludeProduct(delivery.MarketingProduct.ProductWarehouse.Product) {
filtered = append(filtered, delivery)
}
} }
return amount return filtered
} }
@@ -8,34 +8,54 @@ import (
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
// === Response DTO === // === Response DTO ===
type SalesDTO struct { type SalesDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"` RealizationDate time.Time `json:"realization_date"`
Age int `json:"age"` Age int `json:"age"`
DoNumber string `json:"do_number"` Week int `json:"week"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"` DoNumber string `json:"do_number"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Qty float64 `json:"qty"` Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
Weight float64 `json:"weight"` Qty float64 `json:"qty"`
AvgWeight float64 `json:"avg_weight"` Weight float64 `json:"weight"`
Price float64 `json:"price"` AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"` SalesPrice float64 `json:"sales_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` TotalSalesPrice float64 `json:"total_sales_price"`
PaymentStatus string `json:"payment_status"` ActualPrice float64 `json:"actual_price"`
TotalActualPrice float64 `json:"total_actual_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
}
type SummaryDTO struct {
TotalSalesPrice float64 `json:"total_sales_price"`
AvgSalesPrice float64 `json:"avg_sales_price"`
TotalActualPrice float64 `json:"total_actual_price"`
AvgActualPrice float64 `json:"avg_actual_price"`
} }
type PenjualanRealisasiResponseDTO struct { type PenjualanRealisasiResponseDTO struct {
Sales []SalesDTO `json:"sales"` Sales []SalesDTO `json:"sales"`
Summary SummaryDTO `json:"summary"`
} }
// === Mapper Functions === // === Mapper Functions ===
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) productFlags := make([]string, len(e.MarketingProduct.ProductWarehouse.Product.Flags))
for i, f := range e.MarketingProduct.ProductWarehouse.Product.Flags {
productFlags[i] = f.Name
}
var category string
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
category = e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category
}
ageInDay, ageInWeeks := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate, productFlags, category)
var product *productDTO.ProductRelationDTO var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
@@ -55,22 +75,50 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
kandang = &mapped kandang = &mapped
} }
var realizationDate time.Time
if e.DeliveryDate != nil {
realizationDate = *e.DeliveryDate
}
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id) doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{ return SalesDTO{
Id: e.Id, Id: e.Id,
RealizationDate: *e.DeliveryDate, RealizationDate: realizationDate,
Age: age, Age: ageInDay,
DoNumber: doNumber, Week: ageInWeeks,
Product: product, DoNumber: doNumber,
Customer: customer, Product: product,
Qty: e.UsageQty, // Show allocated quantity from FIFO Customer: customer,
Weight: e.TotalWeight, Qty: e.UsageQty,
AvgWeight: e.AvgWeight, Weight: e.TotalWeight,
Price: e.UnitPrice, AvgWeight: e.AvgWeight,
TotalPrice: e.TotalPrice, SalesPrice: e.MarketingProduct.UnitPrice,
Kandang: kandang, TotalSalesPrice: e.MarketingProduct.TotalPrice,
PaymentStatus: "Paid", ActualPrice: e.UnitPrice,
TotalActualPrice: e.TotalPrice,
Kandang: kandang,
}
}
func ToSummaryDto(e []entity.MarketingDeliveryProduct) SummaryDTO {
var totalSalesPrice, totalActualPrice, sumSales, sumActual float64
count := len(e)
for _, item := range e {
totalSalesPrice += item.MarketingProduct.TotalPrice
totalActualPrice += item.TotalPrice
sumSales += item.MarketingProduct.UnitPrice
sumActual += item.UnitPrice
}
return SummaryDTO{
TotalSalesPrice: totalSalesPrice,
TotalActualPrice: totalActualPrice,
AvgSalesPrice: sumSales / float64(count),
AvgActualPrice: sumActual / float64(count),
} }
} }
@@ -82,28 +130,35 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
return result return result
} }
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO { func ToPenjualanRealisasiResponseDTO(e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{ return PenjualanRealisasiResponseDTO{
Sales: ToSalesDTOs(e),
Sales: ToSalesDTOs(e), Summary: ToSummaryDto(e),
} }
} }
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int { func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time, productFlags []string, category string) (int, int) {
if len(realisasi) > 0 {
for _, item := range realisasi {
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
}
}
}
return 0
}
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0 return 0, 0
}
for _, flag := range productFlags {
if flag == string(utils.FlagOVK) ||
flag == string(utils.FlagPakan) ||
flag == string(utils.FlagPreStarter) ||
flag == string(utils.FlagStarter) ||
flag == string(utils.FlagFinisher) ||
flag == string(utils.FlagObat) ||
flag == string(utils.FlagVitamin) ||
flag == string(utils.FlagKimia) ||
flag == string(utils.FlagEkspedisi) ||
flag == string(utils.FlagTelur) ||
flag == string(utils.FlagTelurUtuh) ||
flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) {
return 0, 0
}
} }
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
@@ -113,7 +168,20 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, de
} }
} }
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) diff := deliveryDate.Sub(earliestChickinDate)
ageInWeeks := ageInDays / 7 ageInDays := int(diff.Hours() / 24)
return ageInWeeks
var ageInWeeks int
if ageInDays <= 0 {
ageInWeeks = 0
} else {
if category == string(utils.ProjectFlockCategoryLaying) {
ageInDays = ageInDays + 119
ageInWeeks = ((ageInDays - 1) / 7) + 1
} else {
ageInWeeks = ((ageInDays - 1) / 7) + 1
}
}
return ageInDays, ageInWeeks
} }
@@ -1,6 +1,8 @@
package dto package dto
import ( import (
"encoding/json"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
) )
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto return dto
} }
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO) overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string) latestDateByNonstockID := make(map[uint]string)
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
itemName, itemUOM := getItemInfo(budgets[i].Nonstock) itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM overheadsByNonstockID[nonstockID].UOMName = itemUOM
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price budgetQty := budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price) budgetPrice := budgets[i].Price
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
// Budget division: per kandang view only
if isPerKandang && totalKandangCount > 0 {
budgetQty = budgetQty / float64(totalKandangCount)
budgetTotal = budgetTotal / float64(totalKandangCount)
}
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
} }
for i := range realizations { for i := range realizations {
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
overheadsByNonstockID[nonstockID] = &OverheadDTO{} overheadsByNonstockID[nonstockID] = &OverheadDTO{}
} }
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty qty := realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price) totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
// Farm-level expense division
if realizations[i].ExpenseNonstock.Expense != nil &&
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
if len(projectFlockIDs) > 0 {
totalKandangInAllProjects := 0
for _, pfID := range projectFlockIDs {
if count, exists := projectFlockKandangCountMap[pfID]; exists {
totalKandangInAllProjects += count
}
}
if totalKandangInAllProjects > 0 {
if isPerKandang {
qty = qty / float64(totalKandangInAllProjects)
totalAmount = totalAmount / float64(totalKandangInAllProjects)
} else {
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
perKandangQty := qty / float64(totalKandangInAllProjects)
qty = perKandangQty * float64(totalKandangCount)
totalAmount = perKandangAmount * float64(totalKandangCount)
}
}
}
}
overheadsByNonstockID[nonstockID].ActualQuantity += qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
if overheadsByNonstockID[nonstockID].ItemName == "" { if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock) itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
} }
} }
// === Helper Functions === func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
if projectFlockJSON == "" {
return []uint{}
}
var projectFlocks []uint
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
return []uint{}
}
return projectFlocks
}
func countProjectFlocksInJSON(projectFlockJSON string) int {
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
if len(projectFlocks) == 0 {
return 1
}
return len(projectFlocks)
}
func getItemInfo(nonstock *entity.Nonstock) (string, string) { func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 { if nonstock != nil && nonstock.Id != 0 {
@@ -114,6 +114,17 @@ type ClosingSapronakDTO struct {
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
} }
type ClosingSapronakSummaryItemDTO struct {
Category string `json:"category"`
TotalQty int64 `json:"total_qty"`
Uom UomSummaryDTO `json:"uom"`
}
type UomSummaryDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions for Aggregated Sapronak Response === // === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO { func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
@@ -134,7 +145,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
report = &SapronakReportDTO{} report = &SapronakReportDTO{}
} }
filter := strings.ToUpper(strings.TrimSpace(flag)) normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "PULLET" {
return "DOC"
}
return normalized
}
filter := normalizeFlag(flag)
byFlag := map[string]**SapronakCategoryDTO{} byFlag := map[string]**SapronakCategoryDTO{}
if filter == "" || filter == "DOC" { if filter == "" || filter == "DOC" {
@@ -149,10 +167,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)} result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PAKAN"] = &result.Pakan byFlag["PAKAN"] = &result.Pakan
} }
if filter == "" || filter == "PULLET" {
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PULLET"] = &result.Pullet
}
formatDate := func(t *time.Time) string { formatDate := func(t *time.Time) string {
if t == nil { if t == nil {
@@ -162,7 +176,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
for _, group := range report.Groups { for _, group := range report.Groups {
flagKey := strings.ToUpper(group.Flag) flagKey := normalizeFlag(group.Flag)
ptr := byFlag[flagKey] ptr := byFlag[flagKey]
if ptr == nil || *ptr == nil { if ptr == nil || *ptr == nil {
continue continue
@@ -182,7 +196,11 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
for idx, item := range group.Items { for idx, item := range group.Items {
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName) refKey := strings.TrimSpace(item.NoReferensi)
productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey)
if refKey == "" {
productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal))
}
baseRow := SapronakCategoryRowDTO{ baseRow := SapronakCategoryRowDTO{
ID: idx + 1, ID: idx + 1,
Date: formatDate(item.Tanggal), Date: formatDate(item.Tanggal),
@@ -198,18 +216,51 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
switch strings.ToLower(item.JenisTransaksi) { switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk": case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai if item.Tanggal != nil {
row.Date = formatDate(item.Tanggal)
}
if row.UnitPrice == 0 {
if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk
} else if item.Harga > 0 {
row.UnitPrice = item.Harga
}
}
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
case "pemakaian", "adjustment keluar": case "pemakaian", "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar row.QtyUsed += item.QtyKeluar
case "mutasi keluar": row.TotalAmount += item.QtyKeluar * price
case "mutasi keluar", "penjualan":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyOut += item.QtyKeluar row.QtyOut += item.QtyKeluar
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
default: default:
row.QtyIn += item.QtyMasuk row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai row.TotalAmount += item.Nilai
} if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
if row.QtyIn > 0 { }
row.UnitPrice = row.TotalAmount / row.QtyIn
} }
} }
@@ -230,8 +281,8 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
total += r.TotalAmount total += r.TotalAmount
} }
avg := 0.0 avg := 0.0
if qtyIn > 0 { if qtyUsed > 0 {
avg = total / qtyIn avg = total / qtyUsed
} }
cat.Total = SapronakCategoryTotalDTO{ cat.Total = SapronakCategoryTotalDTO{
Label: label, Label: label,
@@ -246,7 +297,5 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
buildTotals(result.Doc, "TOTAL DOC") buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK") buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN") buildTotals(result.Pakan, "TOTAL PAKAN")
buildTotals(result.Pullet, "TOTAL PULLET")
return result return result
} }
+8 -2
View File
@@ -11,6 +11,7 @@ import (
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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" 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" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -33,13 +34,18 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db) recordingRepo := rRecording.NewRecordingRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
hppCostRepo := commonRepo.NewHppCostRepository(db)
hppService := commonSvc.NewHppService(hppCostRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
closingKeuanganService := sClosing.NewClosingKeuanganService(projectFlockRepo, projectFlockKandangRepo, marketingDeliveryProductRepo, expenseRealizationRepo, projectBudgetRepo, chickinRepo, recordingRepo, hppService, hppCostRepo)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService, sapronakService) ClosingRoutes(router, userService, closingService, sapronakService, closingKeuanganService)
} }
@@ -10,13 +10,16 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ClosingRepository interface { type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
@@ -29,9 +32,10 @@ type ClosingRepository interface {
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, 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) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
} }
@@ -58,10 +62,18 @@ type SapronakRow struct {
DestinationWarehouse string `gorm:"column:destination_warehouse"` DestinationWarehouse string `gorm:"column:destination_warehouse"`
Destination string `gorm:"column:destination"` Destination string `gorm:"column:destination"`
Quantity float64 `gorm:"column:quantity"` Quantity float64 `gorm:"column:quantity"`
UnitID uint `gorm:"column:unit_id"`
Unit string `gorm:"column:unit"` Unit string `gorm:"column:unit"`
Notes string `gorm:"column:notes"` Notes string `gorm:"column:notes"`
} }
type SapronakSummaryRow struct {
Category string `gorm:"column:category"`
TotalQty int64 `gorm:"column:total_qty"`
UomID uint `gorm:"column:uom_id"`
UomName string `gorm:"column:uom_name"`
}
type ExpeditionHPPRow struct { type ExpeditionHPPRow struct {
SupplierName string `gorm:"column:supplier_name"` SupplierName string `gorm:"column:supplier_name"`
TotalAmount float64 `gorm:"column:total_amount"` TotalAmount float64 `gorm:"column:total_amount"`
@@ -73,6 +85,7 @@ type SapronakQueryParams struct {
ProjectFlockKandangIDs []uint ProjectFlockKandangIDs []uint
Limit int Limit int
Offset int Offset int
Search string
} }
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) { func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
@@ -108,14 +121,36 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
unionSQL := strings.Join(unionParts, " UNION ALL ") unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
var totalResults int64 var totalResults int64
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL) countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined%s", unionSQL, searchClause)
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil { countArgs := append(append([]any{}, args...), searchArgs...)
if err := db.Raw(countSQL, countArgs...).Scan(&totalResults).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset) dataArgs := append(append([]any{}, args...), searchArgs...)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL) dataArgs = append(dataArgs, params.Limit, params.Offset)
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined%s ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL, searchClause)
var rows []SapronakRow var rows []SapronakRow
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil { if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
@@ -125,6 +160,79 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
return rows, totalResults, nil return rows, totalResults, nil
} }
func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params SapronakQueryParams) ([]SapronakSummaryRow, error) {
db := r.DB().WithContext(ctx)
var (
unionParts []string
args []any
)
switch params.Type {
case validation.SapronakTypeIncoming:
if len(params.WarehouseIDs) == 0 {
return []SapronakSummaryRow{}, nil
}
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
case validation.SapronakTypeOutgoing:
if len(params.WarehouseIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
args = append(args, params.WarehouseIDs)
}
if len(params.ProjectFlockKandangIDs) > 0 {
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
args = append(args, params.ProjectFlockKandangIDs)
}
if len(unionParts) == 0 {
return []SapronakSummaryRow{}, nil
}
default:
return nil, fmt.Errorf("invalid sapronak type: %s", params.Type)
}
unionSQL := strings.Join(unionParts, " UNION ALL ")
search := strings.TrimSpace(params.Search)
searchClause := ""
var searchArgs []any
if search != "" {
searchClause = `
WHERE (
reference_number ILIKE ?
OR product_name ILIKE ?
OR product_category ILIKE ?
OR source_warehouse ILIKE ?
OR destination_warehouse ILIKE ?
OR CAST(quantity AS TEXT) ILIKE ?
OR unit ILIKE ?
OR notes ILIKE ?
OR transaction_type ILIKE ?
)`
like := "%" + search + "%"
searchArgs = append(searchArgs, like, like, like, like, like, like, like, like, like)
}
querySQL := fmt.Sprintf(`
SELECT
product_category AS category,
CAST(COALESCE(SUM(quantity), 0) AS BIGINT) AS total_qty,
unit_id AS uom_id,
unit AS uom_name
FROM (%s) AS combined%s
GROUP BY product_category, unit_id, unit
ORDER BY product_category ASC, unit ASC
`, unionSQL, searchClause)
queryArgs := append(append([]any{}, args...), searchArgs...)
var rows []SapronakSummaryRow
if err := db.Raw(querySQL, queryArgs...).Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) {
if len(projectFlockKandangIDs) == 0 { if len(projectFlockKandangIDs) == 0 {
return 0, 0, nil return 0, 0, nil
@@ -165,6 +273,23 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
} }
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 { if len(projectFlockKandangIDs) == 0 {
return 0, nil return 0, nil
@@ -299,6 +424,7 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo
Joins("JOIN suppliers s ON s.id = e.supplier_id"). Joins("JOIN suppliers s ON s.id = e.supplier_id").
Where("pfk.project_flock_id = ?", projectFlockID). Where("pfk.project_flock_id = ?", projectFlockID).
Where("e.category = ?", "BOP"). Where("e.category = ?", "BOP").
Where("e.realization_date IS NOT NULL").
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi)))
if projectFlockKandangID != nil && *projectFlockKandangID != 0 { if projectFlockKandangID != nil && *projectFlockKandangID != 0 {
@@ -360,6 +486,7 @@ SELECT
w.name AS destination_warehouse, w.name AS destination_warehouse,
'' AS destination, '' AS destination,
pi.total_qty AS quantity, pi.total_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
COALESCE(p.notes, '') AS notes COALESCE(p.notes, '') AS notes
FROM purchase_items pi FROM purchase_items pi
@@ -408,6 +535,7 @@ SELECT
COALESCE(tw.name, '') AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination, '' AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Stock Refill' AS notes 'Stock Refill' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -454,9 +582,10 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category, ), '') AS product_sub_category,
COALESCE(fw.name, '') AS source_warehouse, COALESCE(fw.name, '') AS source_warehouse,
'' AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
COALESCE(tw.name, '') AS destination, '' AS destination,
std.usage_qty AS quantity, std.usage_qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
'Transfer to other unit' AS notes 'Transfer to other unit' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -503,18 +632,27 @@ SELECT
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
), '') AS product_sub_category, ), '') AS product_sub_category,
w.name AS source_warehouse, w.name AS source_warehouse,
'' AS destination_warehouse, COALESCE(c.name, '') AS destination_warehouse,
'RETAIL CUSTOMER' AS destination, '' AS destination,
mp.qty AS quantity, mp.qty AS quantity,
u.id AS unit_id,
u.name AS unit, u.name AS unit,
m.notes AS notes m.notes AS notes
FROM marketing_products mp FROM marketing_products mp
JOIN marketings m ON m.id = mp.marketing_id JOIN marketings m ON m.id = mp.marketing_id
LEFT JOIN customers c ON c.id = m.customer_id
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
JOIN products prod ON prod.id = pw.product_id JOIN products prod ON prod.id = pw.product_id
JOIN uoms u ON u.id = prod.uom_id JOIN uoms u ON u.id = prod.uom_id
JOIN warehouses w ON w.id = pw.warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ? WHERE pw.project_flock_kandang_id IN ?
AND EXISTS (
SELECT 1
FROM flags f
WHERE f.flagable_id = pw.product_id
AND f.flagable_type = 'products'
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
)
` `
) )
@@ -571,6 +709,23 @@ var (
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet) sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
) )
func (r *ClosingRepositoryImpl) joinSapronakProductFlag(db *gorm.DB, productAlias string) *gorm.DB {
subquery := r.DB().
Table("flags").
Select("DISTINCT ON (flagable_id) flagable_id, name").
Where("flagable_type = ?", entity.FlagableTypeProduct).
Where("name IN ?", sapronakFlagsAll).
Order(fmt.Sprintf(
"flagable_id, CASE WHEN name = '%s' THEN 1 WHEN name = '%s' THEN 2 WHEN name = '%s' THEN 3 WHEN name = '%s' THEN 4 ELSE 5 END",
utils.FlagDOC,
utils.FlagPullet,
utils.FlagPakan,
utils.FlagOVK,
))
return db.Joins("JOIN (?) f ON f.flagable_id = "+productAlias+".id", subquery)
}
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow { func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
m := make(map[uint][]SapronakDetailRow) m := make(map[uint][]SapronakDetailRow)
for _, row := range rows { for _, row := range rows {
@@ -607,11 +762,12 @@ func (r *ClosingRepositoryImpl) usageQuery(
COALESCE(p.product_price, 0) AS default_price COALESCE(p.product_price, 0) AS default_price
`) `)
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
return db. db = 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 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...) Where(where, args...)
db = r.joinSapronakProductFlag(db, "p")
return db
} }
func (r *ClosingRepositoryImpl) fetchSapronakUsage( func (r *ClosingRepositoryImpl) fetchSapronakUsage(
@@ -642,10 +798,10 @@ func (r *ClosingRepositoryImpl) detailQuery(
db := r.withCtx(ctx). db := r.withCtx(ctx).
Table(table). 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 products p ON p.id = pw.product_id")
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
db = r.joinSapronakProductFlag(db, "p")
return db.Select(selectSQL).Where(where, args...) return db.Select(selectSQL).Where(where, args...)
} }
@@ -735,16 +891,84 @@ func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Con
) )
} }
func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
if projectFlockKandangID == 0 {
return map[uint][]SapronakDetailRow{}, nil
}
query := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(
pi.received_date,
st.transfer_date,
lt.transfer_date,
sl.created_at,
pc.chick_in_date,
r.record_datetime
) AS date,
COALESCE(
po.po_number,
st.movement_number,
lt.transfer_number,
CONCAT('ADJ-', ast.id),
CONCAT('CHICKIN-', pc.id),
CAST(r.id AS TEXT),
''
) AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(pi.price, p.product_price, 0) AS price
`).
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
Joins("LEFT JOIN recordings r ON r.id = rs.recording_id").
Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
Joins("LEFT JOIN stock_logs sl ON sl.id = ast.stock_log_id").
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("f.name IN ?", sapronakFlagsAll).
Where(`
(sa.usable_type = ? AND r.project_flock_kandangs_id = ?)
OR
(sa.usable_type = ? AND pc_used.project_flock_kandang_id = ?)
`,
fifo.UsableKeyRecordingStock.String(), projectFlockKandangID,
fifo.UsableKeyProjectChickin.String(), projectFlockKandangID,
)
query = r.joinSapronakProductFlag(query, "p").
Group(`
pw.product_id, p.name, f.name,
pi.received_date, st.transfer_date, lt.transfer_date, sl.created_at, pc.chick_in_date, r.record_datetime,
po.po_number, st.movement_number, lt.transfer_number, ast.id, pc.id, r.id,
pi.price, p.product_price
`)
return scanAndGroupDetails(query)
}
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB { func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
return r.withCtx(ctx). db := r.withCtx(ctx).
Table("purchase_items AS pi"). Table("purchase_items AS pi").
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL"). Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
Joins("JOIN products p ON p.id = pi.product_id"). Joins("JOIN products p ON p.id = pi.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pi.warehouse_id"). Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Where("w.kandang_id = ?", kandangID). Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll). Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL") Where("pi.received_date IS NOT NULL")
return r.joinSapronakProductFlag(db, "p")
} }
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
@@ -815,10 +1039,10 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui
`). `).
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id"). 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).
Joins("JOIN warehouses w ON w.id = pw.warehouse_id") Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
db = applyJoins(db, joins...) db = applyJoins(db, joins...)
db = r.joinSapronakProductFlag(db, "p")
if err := db. if err := db.
Where("sl.loggable_type = ?", logType). Where("sl.loggable_type = ?", logType).
@@ -870,146 +1094,196 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
} }
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) { func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true) incomingQuery := r.withCtx(ctx).
Table("stock_transfer_details AS std").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
COALESCE(std.total_qty, 0) AS qty_in,
0 AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = std.dest_product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Where("w.kandang_id = ?", kandangID).
Where("(fw.kandang_id IS NULL OR fw.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll)
incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
incoming, err := scanAndGroupDetails(incomingQuery)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
return ref
}
return fmt.Sprintf("TRF-%d", row.ID)
})
return in, out, nil
}
type ActualUsageCostRow struct { incomingLayingQuery := r.withCtx(ctx).
ProductID uint `gorm:"column:product_id"` Table("laying_transfer_targets AS ltt").
ProductName string `gorm:"column:product_name"` Select(`
FlagName string `gorm:"column:flag_name"` pw.product_id AS product_id,
TotalQty float64 `gorm:"column:total_qty"` p.name AS product_name,
TotalPrice float64 `gorm:"column:total_price"` f.name AS flag,
AveragePrice float64 `gorm:"column:average_price"` lt.transfer_date::timestamp AS date,
} COALESCE(lt.transfer_number, '') AS reference,
COALESCE(ltt.total_qty, 0) AS qty_in,
func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.Context, projectFlockID uint) ([]ActualUsageCostRow, error) { 0 AS qty_out,
if projectFlockID == 0 { COALESCE(p.product_price, 0) AS price
return []ActualUsageCostRow{}, nil `).
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_sources lts ON lts.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_source ON pw_source.id = lts.product_warehouse_id").
Joins("LEFT JOIN warehouses w_source ON w_source.id = pw_source.warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = ltt.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Where("w.kandang_id = ?", kandangID).
Where("(w_source.kandang_id IS NULL OR w_source.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll)
incomingLayingQuery = r.joinSapronakProductFlag(incomingLayingQuery, "p")
incomingLaying, err := scanAndGroupDetails(incomingLayingQuery)
if err != nil {
return nil, nil, err
}
for pid, rows := range incomingLaying {
incoming[pid] = append(incoming[pid], rows...)
} }
db := r.DB().WithContext(ctx) outgoingQuery := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
std.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
st.transfer_date::timestamp AS date,
COALESCE(st.movement_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN stock_transfer_details std ON std.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyStockTransferOut.String()).
Joins("JOIN stock_transfers st ON st.id = std.stock_transfer_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = std.dest_product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
Joins("JOIN products p ON p.id = std.product_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
Group("std.id, std.product_id, p.name, f.name, st.transfer_date, st.movement_number, p.product_price")
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
outgoing, err := scanAndGroupDetails(outgoingQuery)
if err != nil {
return nil, nil, err
}
// Get all project flock kandang IDs for this project flock outgoingLayingQuery := r.withCtx(ctx).
var pfkIDs []uint Table("stock_allocations AS sa").
err := db.Table("project_flock_kandangs"). Select(`
Where("project_flock_id = ?", projectFlockID). pw.product_id AS product_id,
Pluck("id", &pfkIDs).Error p.name AS product_name,
f.name AS flag,
lt.transfer_date::timestamp AS date,
COALESCE(lt.transfer_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(p.product_price, 0) AS price
`).
Joins("JOIN laying_transfer_sources lts ON lts.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
Joins("JOIN laying_transfers lt ON lt.id = lts.laying_transfer_id").
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.laying_transfer_id = lt.id").
Joins("LEFT JOIN product_warehouses pw_dest ON pw_dest.id = ltt.product_warehouse_id").
Joins("LEFT JOIN warehouses w_dest ON w_dest.id = pw_dest.warehouse_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("w.kandang_id = ?", kandangID).
Where("(w_dest.kandang_id IS NULL OR w_dest.kandang_id <> w.kandang_id)").
Where("f.name IN ?", sapronakFlagsAll).
Group("lts.id, pw.product_id, p.name, f.name, lt.transfer_date, lt.transfer_number, p.product_price")
outgoingLayingQuery = r.joinSapronakProductFlag(outgoingLayingQuery, "p")
outgoingLaying, err := scanAndGroupDetails(outgoingLayingQuery)
if err != nil {
return nil, nil, err
}
for pid, rows := range outgoingLaying {
outgoing[pid] = append(outgoing[pid], rows...)
}
return incoming, outgoing, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
query := r.withCtx(ctx).
Table("stock_allocations AS sa").
Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(mdp.delivery_date, mdp.created_at) AS date,
COALESCE(m.so_number, '') AS reference,
0 AS qty_in,
COALESCE(SUM(sa.qty), 0) AS qty_out,
COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
`).
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN marketings m ON m.id = mp.marketing_id").
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Where("sa.status = ?", entity.StockAllocationStatusActive).
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
query = r.joinSapronakProductFlag(query, "p")
sales, err := scanAndGroupDetails(query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(pfkIDs) == 0 { nonFifoQuery := r.withCtx(ctx).
return []ActualUsageCostRow{}, nil Table("marketing_delivery_products AS mdp").
}
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(` Select(`
pw.product_id AS product_id, pw.product_id AS product_id,
p.name AS product_name, p.name AS product_name,
COALESCE(f.name, tf.name) AS flag_name, f.name AS flag,
COALESCE(SUM( COALESCE(mdp.delivery_date, mdp.created_at) AS date,
CASE COALESCE(m.so_number, '') AS reference,
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) 0 AS qty_in,
WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) COALESCE(mdp.usage_qty, 0) AS qty_out,
ELSE 0 COALESCE(mdp.unit_price, mp.unit_price, 0) AS price
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 marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("JOIN products AS p ON p.id = pw.product_id"). Joins("JOIN marketings m ON m.id = mp.marketing_id").
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id"). Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("JOIN products p ON p.id = pw.product_id").
Where("pc.project_flock_kandang_id IN ?", pfkIDs). Joins("LEFT JOIN stock_allocations sa ON sa.usable_id = mdp.id AND sa.usable_type = ? AND sa.status = ?",
Where("pc.usage_qty > 0"). fifo.UsableKeyMarketingDelivery.String(),
Group("pw.product_id, p.name, f.name") entity.StockAllocationStatusActive,
).
Where("mdp.usage_qty > 0").
Where("sa.id IS NULL").
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
Where("f.name IN ?", sapronakFlagsAll).
Group("mdp.id, pw.product_id, p.name, f.name, mdp.delivery_date, mdp.created_at, m.so_number, mdp.unit_price, mp.unit_price")
var chickinRows []ActualUsageCostRow nonFifoQuery = r.joinSapronakProductFlag(nonFifoQuery, "p")
if err := chickinQuery.Scan(&chickinRows).Error; err != nil { nonFifoSales, err := scanAndGroupDetails(nonFifoQuery)
if err != nil {
return nil, err return nil, err
} }
// Merge results for pid, rows := range nonFifoSales {
rows = append(rows, chickinRows...) sales[pid] = append(sales[pid], rows...)
}
return rows, nil return sales, nil
} }
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) { func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
+7 -2
View File
@@ -9,8 +9,8 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) { func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService, closingKeuanganSvc closing.ClosingKeuanganService) {
ctrl := controller.NewClosingController(s, sapronakSvc) ctrl := controller.NewClosingController(s, sapronakSvc, closingKeuanganSvc)
route := v1.Group("/closings") route := v1.Group("/closings")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
@@ -23,13 +23,18 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan) route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary) 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/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_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/: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("/: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("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:projectFlockId/sapronak/summary", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronakSummary)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) 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("/: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/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan) route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
route.Get("/:project_flock_id/:project_flock_kandang_id/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuanganByKandang)
} }
@@ -2,7 +2,9 @@ package service
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -16,6 +18,7 @@ import (
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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" 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" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -32,12 +35,12 @@ import (
type ClosingService interface { type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) GetClosingSapronakSummary(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
} }
@@ -46,6 +49,7 @@ type closingService struct {
Validate *validator.Validate Validate *validator.Validate
Repository repository.ClosingRepository Repository repository.ClosingRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
@@ -54,14 +58,17 @@ type closingService struct {
ChickinRepo chickinRepository.ProjectChickinRepository ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository RecordingRepo recordingRepository.RecordingRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
} }
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 { func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
return &closingService{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo, MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
@@ -70,6 +77,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo, PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo, RecordingRepo: recordingRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
} }
} }
@@ -90,11 +99,33 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
} }
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
statusFilter := ""
if params.ProjectStatus != nil {
switch *params.ProjectStatus {
case 1:
statusFilter = "Pengajuan"
case 2:
statusFilter = "Aktif"
}
}
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db) db = s.withClosingRelations(db)
if params.LocationID != nil {
db = db.Where("location_id = ?", *params.LocationID)
}
if statusFilter != "" {
latestApprovalSubQuery := s.Repository.DB().
WithContext(c.Context()).
Table("approvals").
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
Where("approvable_type = ?", utils.ApprovalWorkflowProjectFlock.String()).
Order("approvable_id, id DESC")
db = db.Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
Where("LOWER(latest_approval.step_name) = LOWER(?)", statusFilter)
}
if params.Search != "" { if params.Search != "" {
return db.Where("flock_name LIKE ?", "%"+params.Search+"%") return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -129,38 +160,33 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
return projectFlock, nil return projectFlock, nil
} }
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) { func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
return db. if err != nil {
Preload("MarketingProduct"). return nil, err
Preload("MarketingProduct.ProductWarehouse"). }
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory"). realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID, projectFlock.Category)
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(realisasi) == 0 { if len(realisasi) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found") return []entity.MarketingDeliveryProduct{}, nil
} }
return realisasi, nil return realisasi, nil
} }
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
if kandangID != nil {
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
@@ -181,6 +207,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
return &summary, nil return &summary, nil
} }
func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) {
if projectFlockID == 0 || kandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id")
}
db := s.Repository.DB().WithContext(ctx)
var kandang entity.ProjectFlockKandang
if err := db.
Preload("Kandang").
Preload("Kandang.Location").
Preload("Kandang.Pic").
Where("project_flock_id = ?", projectFlockID).
Where("kandang_id = ?", kandangID).
First(&kandang).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
var project entity.ProjectFlock
if err := db.
Select("id", "category").
First(&project, projectFlockID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
if err := db.
Table("project_flock_populations pfp").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pc.project_flock_kandang_id = ?", kandang.Id).
Select("COALESCE(SUM(pfp.total_qty), 0)").
Scan(&population).Error; err != nil {
s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
var chickInDate time.Time
if err := db.
Table("project_chickins").
Where("project_flock_kandang_id = ?", kandang.Id).
Select("MIN(chick_in_date)").
Scan(&chickInDate).Error; err != nil {
s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date")
}
statusProject := "Belum Selesai"
var approvalDate string
if s.ApprovalSvc != nil {
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
if err != nil {
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
}
var (
minStep uint16
latestActionAt time.Time
)
for _, rec := range records {
if minStep == 0 || rec.StepNumber < minStep {
minStep = rec.StepNumber
}
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
latestActionAt = rec.ActionAt
statusProject = rec.StepName
}
}
if statusProject == "" && minStep > 0 {
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok {
statusProject = label
}
}
if !latestActionAt.IsZero() {
approvalDate = latestActionAt.Format("2006-01-02")
}
}
closingDate := ""
if kandang.ClosedAt != nil {
closingDate = kandang.ClosedAt.Format("2006-01-02")
}
chickInDateStr := ""
if !chickInDate.IsZero() {
chickInDateStr = chickInDate.Format("2006-01-02")
}
populationInt := int(population)
return &dto.ClosingSummaryKandangDTO{
FlockID: projectFlockID,
Period: kandang.Period,
LocationName: kandang.Kandang.Location.Name,
Population: populationInt,
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
ProjectType: project.Category,
ClosingDate: closingDate,
KandangName: kandang.Kandang.Name,
ChickInDate: chickInDateStr,
PicName: kandang.Kandang.Pic.Name,
ApprovalDate: approvalDate,
ProjectStatus: statusProject,
}, nil
}
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -220,7 +364,9 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
} }
var projectFlockKandangIDs []uint var projectFlockKandangIDs []uint
if params.Type == validation.SapronakTypeOutgoing { if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
@@ -235,6 +381,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
ProjectFlockKandangIDs: projectFlockKandangIDs, ProjectFlockKandangIDs: projectFlockKandangIDs,
Limit: params.Limit, Limit: params.Limit,
Offset: offset, Offset: offset,
Search: params.Search,
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
@@ -269,6 +416,74 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
return items, totalResults, nil return items, totalResults, nil
} }
func (s closingService) GetClosingSapronakSummary(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakSummaryItemDTO, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if params == nil {
params = &validation.ClosingSapronakQuery{}
}
if err := s.Validate.Struct(params); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
return nil, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
}
s.Log.Errorf("Failed get project flock %d for sapronak closing summary: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
}
var projectFlockKandangIDs []uint
if params.KandangID != nil && *params.KandangID > 0 {
projectFlockKandangIDs = []uint{*params.KandangID}
} else if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
}
rows, err := s.Repository.GetSapronakSummary(c.Context(), repository.SapronakQueryParams{
Type: params.Type,
WarehouseIDs: warehouseIDs,
ProjectFlockKandangIDs: projectFlockKandangIDs,
Search: params.Search,
})
if err != nil {
s.Log.Errorf("Failed to fetch sapronak %s summary for project flock %d: %+v", params.Type, projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak summary data")
}
items := make([]dto.ClosingSapronakSummaryItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, dto.ClosingSapronakSummaryItemDTO{
Category: row.Category,
TotalQty: row.TotalQty,
Uom: dto.UomSummaryDTO{
ID: row.UomID,
Name: row.UomName,
},
})
}
return items, nil
}
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
var kandangIDs []uint var kandangIDs []uint
db := s.Repository.DB().WithContext(ctx) db := s.Repository.DB().WithContext(ctx)
@@ -303,10 +518,10 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
var ids []uint var ids []uint
err := s.Repository.DB().WithContext(ctx). query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}). Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID). Where("project_flock_id = ?", projectFlockID)
Pluck("id", &ids).Error err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -369,126 +584,94 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return statusProject, statusClosing, nil return statusProject, statusClosing, nil
} }
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) { func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
totalKandangCount := len(projectFlockKandangs)
// Build kandang count map for farm expense division
projectFlockKandangCountMap := make(map[uint]int)
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
involvedProjectFlocks := make(map[uint]bool)
for _, realization := range realizations {
if realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Expense != nil &&
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
var projectFlockIDs []uint
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
for _, pfID := range projectFlockIDs {
if pfID != projectFlockID {
involvedProjectFlocks[pfID] = true
}
}
}
}
}
for pfID := range involvedProjectFlocks {
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
projectFlockKandangCountMap[pfID] = len(pfKandangs)
}
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var totalChickinQty float64 var totalChickinQty float64
for _, chickin := range chickins { var totalDepletion float64
totalChickinQty += chickin.UsageQty
}
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if projectFlockKandangID != nil {
if err != nil { for _, chickin := range chickins {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) if chickin.ProjectFlockKandangId == *projectFlockKandangID {
totalChickinQty += chickin.UsageQty
}
}
var depletionResult float64
err = s.RecordingRepo.DB().WithContext(c.Context()).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
Scan(&depletionResult).Error
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
} else {
totalDepletion = depletionResult
}
} else {
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
} }
totalActualPopulation := totalChickinQty - totalDepletion totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
return &result, nil 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) { func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -521,12 +704,28 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
return result, nil return result, nil
} }
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) var projectFlockKandangIDs []uint
if kandangID != nil && *kandangID > 0 {
projectFlockKandangIDs = []uint{*kandangID}
} else {
var err error
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")
}
}
if len(projectFlockKandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
} }
@@ -535,19 +734,33 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
var population float64 population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
for _, history := range project.KandangHistory { if err != nil {
for _, chickin := range history.Chickins { s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
population += chickin.UsageQty + chickin.PendingUsageQty return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
} }
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
}
if !isGrowing && currentWeek != 0 {
currentWeek = currentWeek + 17
}
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data")
}
var fcrActFromRecording *float64
if targetAverages.FcrCount > 0 {
fcrAvg := targetAverages.FcrAvg
fcrActFromRecording = &fcrAvg
} }
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
@@ -556,6 +769,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
} }
averageFeedIntake := targetAverages.FeedIntakeAvg
feedIntakeStd := 0.0
var mortalityStdFromGrowth *float64
if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil {
growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if growthErr != nil {
if !errors.Is(growthErr, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data")
}
} else if growthDetail != nil {
if growthDetail.FeedIntake != nil {
feedIntakeStd = *growthDetail.FeedIntake
}
if growthDetail.MaxDepletion != nil {
mortalityStdFromGrowth = growthDetail.MaxDepletion
}
}
}
var productionStandardDetail *entity.ProductionStandardDetail
if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil {
productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
productionStandardDetail = nil
} else {
s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data")
}
}
}
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
@@ -578,10 +825,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
} }
feedUsedPerHead := 0.0 // feedUsedPerHead := 0.0
if population > 0 { // if population > 0 {
feedUsedPerHead = feedUsed / population // feedUsedPerHead = feedUsed / population
} // }
purchase := dto.ClosingPurchaseDTO{ purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population), InitialPopulation: int(population),
@@ -589,7 +836,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
FinalPopulation: int(finalPopulation), FinalPopulation: int(finalPopulation),
FeedIn: feedIn, FeedIn: feedIn,
FeedUsed: feedUsed, FeedUsed: feedUsed,
FeedUsedPerHead: feedUsedPerHead, // FeedUsedPerHead: feedUsedPerHead,
} }
chickenFlagNames := []string{string(utils.FlagPullet)} chickenFlagNames := []string{string(utils.FlagPullet)}
@@ -622,6 +869,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
} }
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
var eggSales *dto.ClosingEggSalesDTO var eggSales *dto.ClosingEggSalesDTO
var eggPerformance *dto.ClosingPerformanceDTO var eggPerformance *dto.ClosingPerformanceDTO
@@ -669,6 +919,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
} }
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording
}
eggPerformance = &eggPerf eggPerformance = &eggPerf
} }
@@ -685,15 +938,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
DeffMortality: chickenPerformance.DeffMortality, DeffMortality: chickenPerformance.DeffMortality,
} }
if eggPerformance != nil { if eggPerformance != nil {
performance.FcrStd = eggPerformance.FcrStd // performance.FcrStd = eggPerformance.FcrStd
performance.FcrAct = eggPerformance.FcrAct performance.FcrAct = eggPerformance.FcrAct
performance.DeffFcr = eggPerformance.DeffFcr // performance.DeffFcr = eggPerformance.DeffFcr
performance.Awg = eggPerformance.Awg performance.AwgAct = eggPerformance.AwgAct
} else { } else {
performance.FcrStd = chickenPerformance.FcrStd // performance.FcrStd = chickenPerformance.FcrStd
performance.FcrAct = chickenPerformance.FcrAct performance.FcrAct = chickenPerformance.FcrAct
performance.DeffFcr = chickenPerformance.DeffFcr // performance.DeffFcr = chickenPerformance.DeffFcr
performance.Awg = chickenPerformance.Awg performance.AwgAct = chickenPerformance.AwgAct
}
performance.FeedIntake = averageFeedIntake
performance.FeedIntakeStd = feedIntakeStd
if targetAverages.CumDepletionRateCount > 0 {
performance.MortalityAct = targetAverages.CumDepletionRateAvg
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if mortalityStdFromGrowth != nil {
performance.MortalityStd = *mortalityStdFromGrowth
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
if productionStandardDetail != nil {
if productionStandardDetail.StandardFCR != nil {
performance.FcrStd = *productionStandardDetail.StandardFCR
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = *productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = *productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = *productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = *productionStandardDetail.TargetEggMass
}
}
} }
result := dto.ClosingProductionReportDTO{ result := dto.ClosingProductionReportDTO{
@@ -739,6 +1040,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
return totalAgeWeeks / totalQty, nil return totalAgeWeeks / totalQty, nil
} }
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
firstKandangID := projectFlockKandangIDs[0]
var chickin entity.ProjectChickin
if err := s.Repository.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", firstKandangID).
Order("chick_in_date ASC").
First(&chickin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, err
}
recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID)
if err != nil {
return 0, err
}
if recording == nil {
return 0, nil
}
if recording.RecordDatetime.Before(chickin.ChickInDate) {
return 0, nil
}
elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate)
weekFloat := elapsed.Hours() / (24 * 7)
week := int(math.Ceil(weekFloat))
if week <= 0 {
week = 1
}
return week, nil
}
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
@@ -769,7 +1110,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
FcrStd: fcrStd, FcrStd: fcrStd,
FcrAct: fcrAct, FcrAct: fcrAct,
DeffFcr: deffFcr, DeffFcr: deffFcr,
Awg: awg, AwgAct: awg,
} }
} }
@@ -790,53 +1131,3 @@ func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (fl
return closest.Mortality, closest.FcrNumber 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
}
@@ -0,0 +1,506 @@
package service
import (
"errors"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
// ClosingKeuanganService handles closing keuangan business logic
type ClosingKeuanganService interface {
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error)
GetClosingKeuanganByKandang(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error)
}
// CostData holds all cost-related information
type CostData struct {
FeedCost float64
OvkCost float64
ChickenCost float64
ExpeditionCost float64
BudgetOperational float64
RealizationOperational float64
}
// ProductionData holds all production and sales related information
type ProductionData struct {
TotalPopulationIn float64
TotalDepletion float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalWeightSold float64
TotalSalesAmount float64
}
type closingKeuanganService struct {
Log *logrus.Logger
ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
RecordingRepo recordingRepository.RecordingRepository
HppSvc commonSvc.HppService
HppRepo commonRepo.HppCostRepository
}
func NewClosingKeuanganService(
projectFlockRepo projectflockRepository.ProjectflockRepository,
projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository,
marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository,
expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository,
projectBudgetRepo projectflockRepository.ProjectBudgetRepository,
chickinRepo chickinRepository.ProjectChickinRepository,
recordingRepo recordingRepository.RecordingRepository,
hppSvc commonSvc.HppService,
hppRepo commonRepo.HppCostRepository,
) ClosingKeuanganService {
return &closingKeuanganService{
Log: utils.Log,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
HppSvc: hppSvc,
HppRepo: hppRepo,
}
}
func (s closingKeuanganService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); 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")
}
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
}
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs)
}
func (s closingKeuanganService) GetClosingKeuanganByKandang(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID uint) (*dto.ClosingKeuanganData, error) {
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), projectFlockKandangID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
if projectFlockKandang.ProjectFlockId != projectFlockID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang does not belong to this project flock")
}
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
projectFlockKandangs := []entity.ProjectFlockKandang{*projectFlockKandang}
return s.calculateClosingKeuangan(c, projectFlock, projectFlockKandangs)
}
func (s closingKeuanganService) calculateClosingKeuangan(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang) (*dto.ClosingKeuanganData, error) {
var projectFlockKandangIDs []uint
for _, projectFlockKandang := range projectFlockKandangs {
projectFlockKandangIDs = append(projectFlockKandangIDs, projectFlockKandang.Id)
}
isPerKandang := len(projectFlockKandangs) == 1
var projectFlockKandangID *uint
if isPerKandang {
kandangID := projectFlockKandangs[0].Id
projectFlockKandangID = &kandangID
}
costs, err := s.calculateCosts(c, projectFlock, projectFlockKandangs, projectFlockKandangIDs, projectFlockKandangID)
if err != nil {
return nil, err
}
productionData, err := s.calculateProductionData(c, projectFlock, projectFlockKandangIDs, projectFlockKandangID)
if err != nil {
return nil, err
}
hppSection := s.buildHPPSection(c, projectFlock, projectFlockKandangs, costs, productionData)
profitLossSection := s.buildProfitLossSection(projectFlock, costs, productionData)
data := dto.ToClosingKeuanganData(hppSection, profitLossSection)
return &data, nil
}
func (s closingKeuanganService) calculateCosts(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*CostData, error) {
costs := &CostData{}
var err error
costs.FeedCost, err = s.HppRepo.GetFeedUsageCost(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
costs.FeedCost = 0
}
costs.OvkCost, err = s.HppRepo.GetOvkUsageCost(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
costs.OvkCost = 0
}
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
for _, projectFlockKandang := range projectFlockKandangs {
depresiasiCost, err := s.HppSvc.GetDepresiasiTransfer(projectFlockKandang.Id, nil)
if err == nil {
costs.ChickenCost += depresiasiCost
}
pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id)
if err == nil {
costs.ChickenCost += pulletCost
}
}
} else {
for _, projectFlockKandang := range projectFlockKandangs {
pulletCost, err := s.HppRepo.GetPulletCost(c.Context(), projectFlockKandang.Id)
if err == nil {
costs.ChickenCost += pulletCost
}
}
}
costs.ExpeditionCost, err = s.HppRepo.GetExpedisionCost(c.Context(), projectFlockKandangIDs)
if err != nil {
costs.ExpeditionCost = 0
}
if budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlock.Id); err == nil {
totalBudget := 0.0
for _, budget := range budgets {
totalBudget += budget.Price * budget.Qty
}
if projectFlockKandangID != nil {
allKandangs, errKandang := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlock.Id)
if errKandang == nil && len(allKandangs) > 0 {
costs.BudgetOperational = totalBudget / float64(len(allKandangs))
}
} else {
costs.BudgetOperational = totalBudget
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch budgets for project_flock_id=%d: %+v", projectFlock.Id, err)
}
if realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlock.Id, projectFlockKandangID); err == nil {
for _, realization := range realizations {
amount := realization.Price * realization.Qty
isEkspedisi := realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Nonstock != nil &&
containsFlag(realization.ExpenseNonstock.Nonstock.Flags, "EKSPEDISI")
if !isEkspedisi {
costs.RealizationOperational += amount
}
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch realizations for project_flock_id=%d: %+v", projectFlock.Id, err)
}
return costs, nil
}
func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangIDs []uint, projectFlockKandangID *uint) (*ProductionData, error) {
data := &ProductionData{}
var err error
data.TotalPopulationIn, err = s.HppRepo.GetTotalPopulation(c.Context(), projectFlockKandangIDs)
if err != nil {
data.TotalPopulationIn = 0
}
if projectFlockKandangID != nil {
data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
data.TotalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
data.TotalDepletion = 0
}
if projectFlockKandangID != nil {
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockKandangID(c.Context(), *projectFlockKandangID)
} else {
data.TotalWeightProduced, err = s.RecordingRepo.GetTotalWeightProducedFromUniformityByProjectFlockID(c.Context(), projectFlock.Id)
}
if err != nil {
data.TotalWeightProduced = 0
}
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
_, data.TotalEggWeightKg, err = s.HppRepo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(c.Context(), projectFlockKandangIDs, nil)
if err != nil {
data.TotalEggWeightKg = 0
}
}
var deliveryProducts []entity.MarketingDeliveryProduct
if projectFlockKandangID != nil {
deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, projectFlockKandangID, projectFlock.Category)
} else {
deliveryProducts, err = s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlock.Id, nil, projectFlock.Category)
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data penjualan")
}
for _, delivery := range deliveryProducts {
if delivery.MarketingProduct.ProductWarehouse.Product.Id == 0 {
continue
}
data.TotalWeightSold += delivery.TotalWeight
data.TotalSalesAmount += delivery.TotalPrice
}
return data, nil
}
func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection {
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForCalculation = totalEggWeightKg
}
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if weightForCalculation > 0 {
rpPerKg = amount / weightForCalculation
}
return
}
createHPPItem := func(id uint, category, code, label string, budgetAmount, realizationAmount float64) dto.HPPItem {
budgetRpPerBird, budgetRpPerKg := calculateMetrics(budgetAmount)
realizationRpPerBird, realizationRpPerKg := calculateMetrics(realizationAmount)
return dto.ToHPPItem(
id,
category,
code,
label,
dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
)
}
hppItems := []dto.HPPItem{}
hppItems = append(hppItems, createHPPItem(1, "purchase", string(dto.HPPCodePakan), "Pembelian Pakan", costs.FeedCost, costs.FeedCost))
hppItems = append(hppItems, createHPPItem(2, "purchase", string(dto.HPPCodeOVK), "Pembelian OVK", costs.OvkCost, costs.OvkCost))
docCode := string(dto.HPPCodeDOC)
docLabel := "Pembelian DOC"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
docCode = string(dto.HPPCodeDepresiasi)
docLabel = "Depresiasi"
}
hppItems = append(hppItems, createHPPItem(3, "purchase", docCode, docLabel, costs.ChickenCost, costs.ChickenCost))
hppItems = append(hppItems, createHPPItem(4, "overhead", string(dto.HPPCodeOverhead), "Pengeluaran Overhead", costs.BudgetOperational, costs.RealizationOperational))
hppItems = append(hppItems, createHPPItem(5, "overhead", string(dto.HPPCodeEkspedisi), "Beban Ekspedisi", costs.ExpeditionCost, costs.ExpeditionCost))
totalBudgetHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.BudgetOperational + costs.ExpeditionCost
totalRealizationHpp := costs.FeedCost + costs.OvkCost + costs.ChickenCost + costs.RealizationOperational + costs.ExpeditionCost
hppBudgetRpPerBird, hppBudgetRpPerKg := calculateMetrics(totalBudgetHpp)
hppRealizationRpPerBird, hppRealizationRpPerKg := calculateMetrics(totalRealizationHpp)
var eggBudgeting, eggRealization *dto.FinancialMetrics
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
accumulateEggMetrics := func(metrics **dto.FinancialMetrics, amount, rpPerKg float64) {
if *metrics == nil {
*metrics = &dto.FinancialMetrics{
RpPerBird: 0,
RpPerKg: rpPerKg,
Amount: amount,
}
} else {
(*metrics).Amount += amount
if totalEggWeightKg > 0 {
(*metrics).RpPerKg = (*metrics).Amount / totalEggWeightKg
}
}
}
for _, projectFlockKandang := range projectFlockKandangs {
hppResponse, err := s.HppSvc.CalculateHppCost(projectFlockKandang.Id, nil)
if err == nil {
accumulateEggMetrics(&eggBudgeting, hppResponse.Estimation.Total, hppResponse.Estimation.HargaKg)
accumulateEggMetrics(&eggRealization, hppResponse.Real.Total, hppResponse.Real.HargaKg)
}
}
}
hppSummary := dto.ToHPPSummary(
"HPP",
dto.ToFinancialMetrics(hppBudgetRpPerBird, hppBudgetRpPerKg, totalBudgetHpp),
dto.ToFinancialMetrics(hppRealizationRpPerBird, hppRealizationRpPerKg, totalRealizationHpp),
eggBudgeting,
eggRealization,
)
return dto.ToHPPSection(hppItems, hppSummary)
}
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
totalPopulationIn := production.TotalPopulationIn
totalWeightProduced := production.TotalWeightProduced
totalEggWeightKg := production.TotalEggWeightKg
totalSalesAmount := production.TotalSalesAmount
totalWeightSold := production.TotalWeightSold
weightForSales := totalWeightSold
weightForCalculation := totalWeightProduced
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
weightForSales = totalWeightSold
weightForCalculation = totalEggWeightKg
}
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if totalPopulationIn > 0 {
rpPerBird = amount / totalPopulationIn
}
if weightForSales > 0 {
rpPerKg = amount / weightForSales
}
return
}
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
if actualPopulation > 0 {
rpPerBird = amount / actualPopulation
}
if weightForCalculation > 0 {
rpPerKg = amount / weightForCalculation
}
return
}
plItems := []dto.ProfitLossItem{}
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
salesLabel := "Penjualan Ayam"
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
salesLabel = "Penjualan Telur"
}
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSales),
salesLabel,
"income",
salesRpPerBird,
salesRpPerKg,
totalSalesAmount,
))
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
_, sapronakRpPerKg := calculateMetrics(totalSapronakAmount)
sapronakRpPerBird := 0.0
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
rpPerBird, _ := calculateMetrics(amount)
sapronakRpPerBird += rpPerBird
}
sapronakLabel := "Pengeluaran Sapronak"
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeSapronak),
sapronakLabel,
"purchase",
sapronakRpPerBird,
sapronakRpPerKg,
totalSapronakAmount,
))
overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeOverhead),
"Overhead",
"overhead",
overheadRpPerBird,
overheadRpPerKg,
costs.RealizationOperational,
))
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost)
plItems = append(plItems, dto.ToProfitLossItem(
string(dto.PLCodeEkspedisi),
"Ekspedisi",
"overhead",
ekspedisiRpPerBird,
ekspedisiRpPerKg,
costs.ExpeditionCost,
))
costOfGoodsSold := costs.ChickenCost + costs.FeedCost + costs.OvkCost
costOfGoodsSoldRpPerBird := sapronakRpPerBird
costOfGoodsSoldRpPerKg := sapronakRpPerKg
grossProfit := totalSalesAmount - costOfGoodsSold
grossProfitRpPerBird := salesRpPerBird - costOfGoodsSoldRpPerBird
grossProfitRpPerKg := salesRpPerKg - costOfGoodsSoldRpPerKg
totalOperatingExpenses := costs.RealizationOperational + costs.ExpeditionCost
totalOperatingExpensesRpPerBird := overheadRpPerBird + ekspedisiRpPerBird
totalOperatingExpensesRpPerKg := overheadRpPerKg + ekspedisiRpPerKg
netProfit := grossProfit - totalOperatingExpenses
netProfitRpPerBird := grossProfitRpPerBird - totalOperatingExpensesRpPerBird
netProfitRpPerKg := grossProfitRpPerKg - totalOperatingExpensesRpPerKg
plSummary := dto.ToProfitLossSummary(
dto.ToFinancialMetrics(grossProfitRpPerBird, grossProfitRpPerKg, grossProfit),
dto.ToFinancialMetrics(totalOperatingExpensesRpPerBird, totalOperatingExpensesRpPerKg, totalOperatingExpenses),
dto.ToFinancialMetrics(netProfitRpPerBird, netProfitRpPerKg, netProfit),
)
return dto.ToProfitLossSection(plItems, plSummary)
}
func containsFlag(flags []entity.Flag, name string) bool {
for _, flag := range flags {
if flag.Name == name {
return true
}
}
return false
}
@@ -2,8 +2,8 @@ package service
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"time"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -112,7 +112,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
} }
// We no longer filter by date for closing sapronak report; pass nil pointers. // We no longer filter by date for closing sapronak report; pass nil pointers.
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag) items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, params.Flag)
if err != nil { if err != nil {
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err) s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
@@ -126,8 +126,6 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
KandangName: pfk.Kandang.Name, KandangName: pfk.Kandang.Name,
Period: pfk.Period, Period: pfk.Period,
Status: status, Status: status,
StartDate: nil,
EndDate: nil,
TotalIncomingValue: totalIncoming, TotalIncomingValue: totalIncoming,
TotalUsageValue: totalUsage, TotalUsageValue: totalUsage,
Items: items, Items: items,
@@ -265,6 +263,7 @@ type sapronakDetailMaps struct {
AdjOutgoing map[uint][]dto.SapronakDetailDTO AdjOutgoing map[uint][]dto.SapronakDetailDTO
TransferIn map[uint][]dto.SapronakDetailDTO TransferIn map[uint][]dto.SapronakDetailDTO
TransferOut map[uint][]dto.SapronakDetailDTO TransferOut map[uint][]dto.SapronakDetailDTO
SalesOut map[uint][]dto.SapronakDetailDTO
} }
func buildSapronakDetails( func buildSapronakDetails(
@@ -274,6 +273,7 @@ func buildSapronakDetails(
adjOutgoingRows map[uint][]repository.SapronakDetailRow, adjOutgoingRows map[uint][]repository.SapronakDetailRow,
transferInRows map[uint][]repository.SapronakDetailRow, transferInRows map[uint][]repository.SapronakDetailRow,
transferOutRows map[uint][]repository.SapronakDetailRow, transferOutRows map[uint][]repository.SapronakDetailRow,
salesOutRows map[uint][]repository.SapronakDetailRow,
) sapronakDetailMaps { ) sapronakDetailMaps {
result := sapronakDetailMaps{ result := sapronakDetailMaps{
Incoming: make(map[uint][]dto.SapronakDetailDTO), Incoming: make(map[uint][]dto.SapronakDetailDTO),
@@ -282,6 +282,7 @@ func buildSapronakDetails(
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO), AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
TransferIn: make(map[uint][]dto.SapronakDetailDTO), TransferIn: make(map[uint][]dto.SapronakDetailDTO),
TransferOut: make(map[uint][]dto.SapronakDetailDTO), TransferOut: make(map[uint][]dto.SapronakDetailDTO),
SalesOut: make(map[uint][]dto.SapronakDetailDTO),
} }
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) { addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
@@ -314,11 +315,12 @@ func buildSapronakDetails(
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false) addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
return result return result
} }
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
// For sapronak closing report we intentionally ignore date range // For sapronak closing report we intentionally ignore date range
// and aggregate all historical transactions for the kandang/project. // and aggregate all historical transactions for the kandang/project.
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId) incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
@@ -345,6 +347,14 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
usageAllocatedDetails, err := s.Repository.FetchSapronakUsageAllocatedDetails(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
if len(usageAllocatedDetails) > 0 {
usageDetailsRows = usageAllocatedDetails
chickinUsageDetailsRows = map[uint][]repository.SapronakDetailRow{}
}
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId) adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
@@ -353,13 +363,49 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if err != nil { if err != nil {
return nil, nil, 0, 0, err return nil, nil, 0, 0, err
} }
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.Id)
if err != nil {
return nil, nil, 0, 0, err
}
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter)) filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
matchesFlag := func(f string) bool { matchesFlag := func(f string) bool {
if filterFlag == "" { if filterFlag == "" {
return true return true
} }
return strings.ToUpper(f) == filterFlag candidate := strings.ToUpper(f)
if filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "DOC" || candidate == "PULLET"
}
return candidate == filterFlag
}
dedupTransfers := func(src map[uint][]dto.SapronakDetailDTO) map[uint][]dto.SapronakDetailDTO {
result := make(map[uint][]dto.SapronakDetailDTO, len(src))
seen := make(map[string]struct{})
for pid, rows := range src {
for _, d := range rows {
dateKey := ""
if d.Tanggal != nil {
dateKey = d.Tanggal.Format("2006-01-02")
}
qtyKey := d.QtyMasuk
if qtyKey == 0 {
qtyKey = d.QtyKeluar
}
ref := strings.TrimSpace(d.NoReferensi)
key := fmt.Sprintf("%d|%s|%s|%.3f", pid, ref, dateKey, qtyKey)
if ref == "" {
key = fmt.Sprintf("%d|%s|%s|%.3f|%s", pid, ref, dateKey, qtyKey, strings.ToUpper(strings.TrimSpace(d.Flag)))
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result[pid] = append(result[pid], d)
}
}
return result
} }
// For project flocks with category GROWING, pullet usage from chickin // For project flocks with category GROWING, pullet usage from chickin
@@ -399,13 +445,17 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...) usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
} }
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows) detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows, salesOutRows)
incomingDetails := detailMaps.Incoming incomingDetails := detailMaps.Incoming
usageDetails := detailMaps.Usage usageDetails := detailMaps.Usage
adjIncoming := detailMaps.AdjIncoming adjIncoming := detailMaps.AdjIncoming
adjOutgoing := detailMaps.AdjOutgoing adjOutgoing := detailMaps.AdjOutgoing
transIncoming := detailMaps.TransferIn transIncoming := detailMaps.TransferIn
transOutgoing := detailMaps.TransferOut transOutgoing := detailMaps.TransferOut
salesOutgoing := detailMaps.SalesOut
transIncoming = dedupTransfers(transIncoming)
transOutgoing = dedupTransfers(transOutgoing)
ensureGroup := func(flag string) *dto.SapronakGroupDTO { ensureGroup := func(flag string) *dto.SapronakGroupDTO {
if g, ok := groupMap[flag]; ok { if g, ok := groupMap[flag]; ok {
@@ -415,6 +465,22 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
return groupMap[flag] return groupMap[flag]
} }
resolveFlagName := func(productID uint, details []dto.SapronakDetailDTO) (string, string) {
flag := ""
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if flag == "" && len(details) > 0 {
flag = details[0].Flag
}
if name == "" && len(details) > 0 {
name = details[0].ProductName
}
return flag, name
}
for _, row := range incoming { for _, row := range incoming {
if !matchesFlag(row.Flag) { if !matchesFlag(row.Flag) {
continue continue
@@ -550,19 +616,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range incomingDetails { for productID, details := range incomingDetails {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai group.TotalNilai += d.Nilai
@@ -571,19 +636,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range adjIncoming { for productID, details := range adjIncoming {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai group.TotalNilai += d.Nilai
@@ -592,19 +656,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range usageDetails { for productID, details := range usageDetails {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar
@@ -612,19 +675,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range adjOutgoing { for productID, details := range adjOutgoing {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar
@@ -632,19 +694,18 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range transIncoming { for productID, details := range transIncoming {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalMasuk += d.QtyMasuk group.TotalMasuk += d.QtyMasuk
group.TotalNilai += d.Nilai group.TotalNilai += d.Nilai
@@ -653,19 +714,37 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
} }
for productID, details := range transOutgoing { for productID, details := range transOutgoing {
flag := "" flag, name := resolveFlagName(productID, details)
name := ""
if item, ok := itemMap[productID]; ok {
flag = item.Flag
name = item.ProductName
}
if !matchesFlag(flag) { if !matchesFlag(flag) {
continue continue
} }
group := ensureGroup(flag) group := ensureGroup(flag)
for _, d := range details { for _, d := range details {
d.Flag = flag if d.Flag == "" {
d.ProductName = name d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar
}
}
for productID, details := range salesOutgoing {
flag, name := resolveFlagName(productID, details)
if !matchesFlag(flag) {
continue
}
group := ensureGroup(flag)
for _, d := range details {
if d.Flag == "" {
d.Flag = flag
}
if d.ProductName == "" {
d.ProductName = name
}
group.Items = append(group.Items, d) group.Items = append(group.Items, d)
group.TotalKeluar += d.QtyKeluar group.TotalKeluar += d.QtyKeluar
group.SaldoAkhir -= d.QtyKeluar group.SaldoAkhir -= d.QtyKeluar
@@ -9,9 +9,11 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
ProjectStatus *int `query:"project_status" validate:"omitempty,oneof=1 2"`
LocationID *uint `query:"location_id" validate:"omitempty,gt=0"`
} }
const ( const (
@@ -20,7 +22,9 @@ const (
) )
type ClosingSapronakQuery struct { type ClosingSapronakQuery struct {
Type string `query:"type" validate:"required,oneof=incoming outgoing"` Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
} }
@@ -83,7 +83,7 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"KANDANG", "KANDANG",
}, },
"stock_log": map[string][]string{ "stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"}, "log_types": []string{"TRANSFER", "ADJUSTMENT", "MARKETING", "CHICKIN", "PURCHASE", "RECORDING"},
"transaction_types": []string{"INCREASE", "DECREASE"}, "transaction_types": []string{"INCREASE", "DECREASE"},
}, },
"supplier_categories": []string{ "supplier_categories": []string{
@@ -74,6 +74,7 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
Name: name, Name: name,
Status: status, Status: status,
Category: item.Category, Category: item.Category,
RejectReason: item.RejectReason,
Date: item.Date, Date: item.Date,
Kandang: kandang, Kandang: kandang,
CreatedUser: nil, CreatedUser: nil,
@@ -150,6 +151,10 @@ func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{ performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
EmployeeID: summary.EmployeeID, EmployeeID: summary.EmployeeID,
EmployeeName: summary.EmployeeName, EmployeeName: summary.EmployeeName,
Kandang: dto.DailyChecklistReportEntityDTO{
Id: summary.KandangID,
Name: summary.KandangName,
},
} }
} }
@@ -303,12 +308,22 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
return err return err
} }
documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs))
for i, doc := range detail.DocumentURLs {
documentDTOs[i] = dto.DailyChecklistDocumentDTO{
Id: doc.ID,
Name: doc.Name,
Size: doc.Size,
URL: doc.URL,
}
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get dailyChecklist successfully", Message: "Get dailyChecklist successfully",
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress), Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
}) })
} }
@@ -342,6 +357,12 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
} }
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
@@ -31,6 +31,7 @@ type DailyChecklistListDTO struct {
TotalPhase int `json:"total_phase"` TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"` TotalActivity int `json:"total_activity"`
Progress int `json:"progress"` Progress int `json:"progress"`
RejectReason *string `json:"reject_reason"`
} }
type DailyChecklistDetailDTO struct { type DailyChecklistDetailDTO struct {
@@ -40,6 +41,14 @@ type DailyChecklistDetailDTO struct {
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"` AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
TotalActivity int `json:"total_activity"` TotalActivity int `json:"total_activity"`
Progress float64 `json:"progress"` Progress float64 `json:"progress"`
DocumentURLs []DailyChecklistDocumentDTO `json:"document_urls"`
}
type DailyChecklistDocumentDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Size float64 `json:"size"`
URL string `json:"url"`
} }
type DailyChecklistSummaryDTO struct { type DailyChecklistSummaryDTO struct {
@@ -55,11 +64,12 @@ type DailyChecklistSummaryDTO struct {
} }
type DailyChecklistPerformanceOverviewDTO struct { type DailyChecklistPerformanceOverviewDTO struct {
EmployeeID uint `json:"employee_id"` EmployeeID uint `json:"employee_id"`
EmployeeName string `json:"employee_name"` EmployeeName string `json:"employee_name"`
TotalActivity int `json:"total_activity"` Kandang DailyChecklistReportEntityDTO `json:"kandang"`
ActivityDone int `json:"activity_done"` TotalActivity int `json:"total_activity"`
ActivityLeft int `json:"activity_left"` ActivityDone int `json:"activity_done"`
ActivityLeft int `json:"activity_left"`
} }
type DailyChecklistReportDTO struct { type DailyChecklistReportDTO struct {
@@ -165,10 +175,11 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
TotalPhase: 0, TotalPhase: 0,
TotalActivity: 0, TotalActivity: 0,
Progress: 0, Progress: 0,
RejectReason: e.RejectReason,
} }
} }
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO { func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases)) phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
for _, phase := range phases { for _, phase := range phases {
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{ phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
@@ -228,5 +239,6 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.
AssignedEmployees: assignedDTOs, AssignedEmployees: assignedDTOs,
TotalActivity: totalActivities, TotalActivity: totalActivities,
Progress: progress, Progress: progress,
DocumentURLs: documentURLs,
} }
} }
+11 -1
View File
@@ -1,10 +1,15 @@
package dailyChecklists package dailyChecklists
import ( import (
"context"
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -19,8 +24,13 @@ func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db) dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db) phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate) dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
DailyChecklistRoutes(router, userService, dailyChecklistService) DailyChecklistRoutes(router, userService, dailyChecklistService)
+17 -17
View File
@@ -1,7 +1,7 @@
package dailyChecklists package dailyChecklists
import ( 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/daily-checklists/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,51 +13,51 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
ctrl := controller.NewDailyChecklistController(s) ctrl := controller.NewDailyChecklistController(s)
route := v1.Group("/daily-checklists") route := v1.Group("/daily-checklists")
// route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_DailyChecklistGetAll), ctrl.GetAll)
route.Get("/report", ctrl.GetReport) route.Get("/report", m.RequirePermissions(m.P_DailyChecklistReports), ctrl.GetReport)
route.Get("/summary", ctrl.GetSummary) route.Get("/summary", m.RequirePermissions(m.P_DailyChecklistDashboardList), ctrl.GetSummary)
route.Get("/report", ctrl.GetReport) // route.Get("/report", ctrl.GetReport)
// create daily checklist // upsert daily checklist
route.Post("/", ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateOne)
// get detail data daily checklist by id // get detail data daily checklist by id
route.Get("/relation/:idDailyChecklist", ctrl.GetOne) route.Get("/relation/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistGetOne), ctrl.GetOne)
// get phases by daily checklist id // get phases by daily checklist id
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist) route.Get("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetPhaseByIdChecklist)
// create task // create task
/* /*
ketika add phase ketika add phase
*/ */
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase) route.Post("/phase/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateDailyChecklistPhase)
// create assigment // create assigment
/* /*
ketika add ABK ketika add ABK
*/ */
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment) route.Post("/assignment/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.CreateAssignment)
// remove assignment // remove assignment
/* /*
ketika remove ABK ketika remove ABK
*/ */
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment) route.Delete("/:idDailyChecklist/assignments/:idEmployee", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.RemoveAssignment)
//get all tasks //get all tasks
route.Get("/tasks", ctrl.GetAllTasks) route.Get("/tasks", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.GetAllTasks)
// update assignment // update assignment
/* /*
ketika check dan uncheck tugas oleh ABK ketika check dan uncheck tugas oleh ABK
*/ */
route.Post("/assignment", ctrl.UpdateAssignment) route.Post("/assignment", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateAssignment)
route.Patch("/:idDailyChecklist", ctrl.UpdateOne) route.Patch("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateOne)
route.Delete("/:idDailyChecklist", ctrl.DeleteOne) route.Delete("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.DeleteOne)
} }
@@ -9,6 +9,7 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -17,6 +18,7 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
@@ -39,10 +41,18 @@ type DailyChecklistService interface {
} }
type dailyChecklistService struct { type dailyChecklistService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.DailyChecklistRepository Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository PhaseRepo phaseRepo.PhasesRepository
DocumentSvc commonSvc.DocumentService
}
type DailyChecklistDocument struct {
ID uint
Name string
Size float64
URL string
} }
type DailyChecklistDetail struct { type DailyChecklistDetail struct {
@@ -52,6 +62,7 @@ type DailyChecklistDetail struct {
AssignedEmployees []entity.Employee AssignedEmployees []entity.Employee
TotalActivities int TotalActivities int
Progress float64 Progress float64
DocumentURLs []DailyChecklistDocument
} }
type DailyChecklistListItem struct { type DailyChecklistListItem struct {
@@ -60,6 +71,7 @@ type DailyChecklistListItem struct {
Date time.Time Date time.Time
Category string Category string
Status *string Status *string
RejectReason *string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Kandang entity.Kandang Kandang entity.Kandang
@@ -108,12 +120,13 @@ type DailyChecklistReportCategory struct {
Baik int Baik int
} }
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService {
return &dailyChecklistService{ return &dailyChecklistService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
PhaseRepo: phaseRepo, PhaseRepo: phaseRepo,
DocumentSvc: documentSvc,
} }
} }
@@ -158,7 +171,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
if params.Search != "" { if params.Search != "" {
like := "%" + params.Search + "%" like := "%" + params.Search + "%"
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like) db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
} }
countDB := db.Session(&gorm.Session{}) countDB := db.Session(&gorm.Session{})
@@ -174,6 +187,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date time.Time Date time.Time
Category string Category string
Status *string Status *string
RejectReason *string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
KandangID uint KandangID uint
@@ -192,6 +206,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
dc.date, dc.date,
dc.category, dc.category,
dc.status, dc.status,
dc.reject_reason,
dc.created_at, dc.created_at,
dc.updated_at, dc.updated_at,
dc.kandang_id, dc.kandang_id,
@@ -265,6 +280,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date: row.Date, Date: row.Date,
Category: row.Category, Category: row.Category,
Status: row.Status, Status: row.Status,
RejectReason: row.RejectReason,
CreatedAt: row.CreatedAt, CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt, UpdatedAt: row.UpdatedAt,
Kandang: kandangMap[row.KandangID], Kandang: kandangMap[row.KandangID],
@@ -345,6 +361,29 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100) progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
} }
documentURLs := make([]DailyChecklistDocument, 0)
if s.DocumentSvc != nil {
documents, err := s.DocumentSvc.ListByTarget(c.Context(), string(utils.DocumentTypeDailyChecklist), uint64(id))
if err != nil {
s.Log.Errorf("Failed to list documents for daily checklist %d: %+v", id, err)
return nil, err
}
for _, doc := range documents {
url, err := s.DocumentSvc.PresignURL(c.Context(), doc, 0)
if err != nil {
s.Log.Errorf("Failed to presign document %d for daily checklist %d: %+v", doc.Id, id, err)
continue
}
documentURLs = append(documentURLs, DailyChecklistDocument{
ID: doc.Id,
Name: doc.Name,
Size: doc.Size,
URL: url,
})
}
}
return &DailyChecklistDetail{ return &DailyChecklistDetail{
Checklist: *checklist, Checklist: *checklist,
Phases: phases, Phases: phases,
@@ -352,6 +391,7 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
AssignedEmployees: assignedEmployees, AssignedEmployees: assignedEmployees,
TotalActivities: totalActivities, TotalActivities: totalActivities,
Progress: progress, Progress: progress,
DocumentURLs: documentURLs,
}, nil }, nil
} }
@@ -377,7 +417,7 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{ err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}}, Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}), DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
}).Create(createBody).Error }).Create(createBody).Error
if err != nil { if err != nil {
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err) s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
@@ -392,6 +432,22 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
return nil, err return nil, err
} }
deletedIDs := make([]uint, 0)
if req.DeletedDocumentIDs != nil {
parts := strings.Split(*req.DeletedDocumentIDs, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
parsedID, err := strconv.ParseUint(part, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid deleted_document_ids")
}
deletedIDs = append(deletedIDs, uint(parsedID))
}
}
updateBody := map[string]any{ updateBody := map[string]any{
"status": req.Status, "status": req.Status,
} }
@@ -400,6 +456,40 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
updateBody["reject_reason"] = *req.RejectReason updateBody["reject_reason"] = *req.RejectReason
} }
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
if len(deletedIDs) > 0 && s.DocumentSvc != nil {
if err := s.DocumentSvc.DeleteDocuments(c.Context(), deletedIDs, true); err != nil {
s.Log.Errorf("Failed to delete daily checklist documents: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete daily checklist documents")
}
}
if len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeDailyChecklist),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentTypeDailyChecklist),
DocumentableID: uint64(id),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
s.Log.Errorf("Failed to upload daily checklist documents: %+v", err)
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload daily checklist documents")
}
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -869,7 +959,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
Joins("JOIN areas a ON a.id = loc.area_id"). Joins("JOIN areas a ON a.id = loc.area_id").
Joins("JOIN phases p ON p.id = dcat.phase_id"). Joins("JOIN phases p ON p.id = dcat.phase_id").
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month). Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year) Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
Where("dc.status = ?", "APPROVED")
if params.AreaID != nil { if params.AreaID != nil {
db = db.Where("a.id = ?", *params.AreaID) db = db.Where("a.id = ?", *params.AreaID)
@@ -1,5 +1,9 @@
package validation package validation
import (
"mime/multipart"
)
type Create struct { type Create struct {
Date string `json:"date" validate:"required"` Date string `json:"date" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"` KandangId uint `json:"kandang_id" validate:"required"`
@@ -8,8 +12,10 @@ type Create struct {
} }
type Update struct { type Update struct {
Status string `json:"status" validate:"required"` Status string `form:"status" json:"status" validate:"required"`
RejectReason *string `json:"reject_reason"` RejectReason *string `form:"reject_reason" json:"reject_reason"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"`
} }
type Query struct { type Query struct {
@@ -46,7 +52,7 @@ type SummaryQuery struct {
type ReportQuery struct { type ReportQuery struct {
Page int `query:"page" validate:"required,number,min=1,gt=0"` Page int `query:"page" validate:"required,number,min=1,gt=0"`
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"required,number,min=1,gt=0"`
Month int `query:"bulan" validate:"required,number,min=1,max=12"` Month int `query:"bulan" validate:"required,number,min=1,max=12"`
Year int `query:"tahun" validate:"required,number,min=1900"` Year int `query:"tahun" validate:"required,number,min=1900"`
AreaID *uint `query:"area_id" validate:"omitempty"` AreaID *uint `query:"area_id" validate:"omitempty"`
@@ -0,0 +1,206 @@
package controller
import (
"math"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type DashboardController struct {
DashboardService service.DashboardService
}
func NewDashboardController(dashboardService service.DashboardService) *DashboardController {
return &DashboardController{
DashboardService: dashboardService,
}
}
func (u *DashboardController) GetAll(c *fiber.Ctx) error {
parseStringListParam := func(param string) ([]string, error) {
if param == "" {
return nil, nil
}
parts := strings.Split(param, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
result = append(result, trimmed)
}
return result, nil
}
parseUintListParam := func(param string) ([]uint, error) {
if param == "" {
return nil, nil
}
parts := strings.Split(param, ",")
ids := make([]uint, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed == "" {
return nil, strconv.ErrSyntax
}
parsed, err := strconv.ParseUint(trimmed, 10, 64)
if err != nil {
return nil, err
}
ids = append(ids, uint(parsed))
}
return ids, nil
}
lokasiIds, err := parseUintListParam(c.Query("location_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_ids")
}
flockIds, err := parseUintListParam(c.Query("flock_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid flock_ids")
}
kandangIds, err := parseUintListParam(c.Query("kandang_ids", ""))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_ids")
}
include, err := parseStringListParam(strings.ToLower(c.Query("include", "")))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid include")
}
analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview)))
metric := strings.ToLower(strings.TrimSpace(c.Query("metric", "")))
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search", "")),
PerformanceOverviewFilter: validation.PerformanceOverviewFilter{
StartDate: c.Query("start_date", ""),
EndDate: c.Query("end_date", ""),
AnalysisMode: analysisMode,
ComparisonType: strings.ToUpper(strings.TrimSpace(c.Query("comparison_type", ""))),
Metric: metric,
LokasiIds: lokasiIds,
FlockIds: flockIds,
KandangIds: kandangIds,
Include: include,
},
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if query.AnalysisMode == validation.AnalysisModeComparison && query.ComparisonType == "" {
return fiber.NewError(fiber.StatusBadRequest, "comparison_type is required for comparison mode")
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
startDate, endDate, endExclusive, err := parsePeriodDates(query.StartDate, query.EndDate, location)
if err != nil {
return err
}
query.PeriodStart = startDate
query.PeriodEnd = endDate
query.PeriodEndExclusive = endExclusive
result, totalResults, err := u.DashboardService.GetAll(c.Context(), query)
if err != nil {
return err
}
hasFilter := query.StartDate != "" ||
query.EndDate != "" ||
len(query.LokasiIds) > 0 ||
len(query.FlockIds) > 0 ||
len(query.KandangIds) > 0 ||
len(query.Include) > 0 ||
query.ComparisonType != "" ||
query.Metric != "" ||
query.AnalysisMode != validation.AnalysisModeOverview
var filters interface{}
if hasFilter {
filters = dto.DashboardFiltersDTO{
StartDate: query.StartDate,
EndDate: query.EndDate,
AnalysisMode: query.AnalysisMode,
ComparisonType: query.ComparisonType,
Metric: query.Metric,
LokasiIds: defaultUintSlice(query.LokasiIds),
FlockIds: defaultUintSlice(query.FlockIds),
KandangIds: defaultUintSlice(query.KandangIds),
Include: query.Include,
}
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithMeta{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dashboard successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
Filters: filters,
},
Data: result,
})
}
func defaultUintSlice(values []uint) []uint {
if values == nil {
return []uint{}
}
return values
}
func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) {
now := time.Now().In(location)
startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
if startDateRaw != "" {
parsed, err := time.ParseInLocation("2006-01-02", startDateRaw, location)
if err != nil {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "start_date must follow format YYYY-MM-DD")
}
startDate = parsed
}
if endDateRaw != "" {
parsed, err := time.ParseInLocation("2006-01-02", endDateRaw, location)
if err != nil {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must follow format YYYY-MM-DD")
}
endDate = parsed
}
if endDate.Before(startDate) {
return time.Time{}, time.Time{}, time.Time{}, fiber.NewError(fiber.StatusBadRequest, "end_date must be greater than or equal to start_date")
}
endExclusive := endDate.AddDate(0, 0, 1)
return startDate, endDate, endExclusive, nil
}

Some files were not shown because too many files have changed in this diff Show More