From e6094528b569bf9600e74f536d2a130e0840fe68 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 8 Dec 2025 17:30:11 +0700 Subject: [PATCH 01/31] add project flock middleware --- internal/middleware/auth.go | 73 ++++++++++++++- internal/middleware/permissions.go | 91 +++++-------------- .../project-flock-kandangs/route.go | 4 +- .../production/project_flocks/route.go | 18 ++-- internal/modules/users/route.go | 8 +- 5 files changed, 106 insertions(+), 88 deletions(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 881c3a67..cf5ce1f3 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -3,14 +3,13 @@ package middleware import ( "strings" - "gitlab.com/mbugroup/lti-api.git/internal/config" + "github.com/gofiber/fiber/v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" - - "github.com/gofiber/fiber/v2" + "gitlab.com/mbugroup/lti-api.git/internal/config" ) const ( @@ -199,3 +198,71 @@ func hasAllScopes(have, required []string) bool { } return true } + +// RequirePermissions ensures the authenticated user possesses all specified permissions. +func RequirePermissions(perms ...string) fiber.Handler { + required := canonicalPermissions(perms) + return func(c *fiber.Ctx) error { + if len(required) == 0 { + return c.Next() + } + + ctx, ok := AuthDetails(c) + if !ok || ctx == nil { + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } + + userPerms := ctx.permissionSet() + if len(userPerms) == 0 { + return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") + } + + for _, perm := range required { + if _, has := userPerms[perm]; !has { + return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") + } + } + + return c.Next() + } +} + +// HasPermission reports whether the current request context includes the given permission. +func HasPermission(c *fiber.Ctx, perm string) bool { + ctx, ok := AuthDetails(c) + if !ok || ctx == nil { + return false + } + perm = canonicalPermission(perm) + if perm == "" { + return false + } + _, has := ctx.permissionSet()[perm] + return has +} + +func (a *AuthContext) permissionSet() map[string]struct{} { + if a == nil || a.Permissions == nil { + return nil + } + return a.Permissions +} + +func canonicalPermissions(perms []string) []string { + out := make([]string, 0, len(perms)) + seen := make(map[string]struct{}, len(perms)) + for _, perm := range perms { + if canonical := canonicalPermission(perm); canonical != "" { + if _, ok := seen[canonical]; ok { + continue + } + seen[canonical] = struct{}{} + out = append(out, canonical) + } + } + return out +} + +func canonicalPermission(perm string) string { + return strings.ToLower(strings.TrimSpace(perm)) +} \ No newline at end of file diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 3ebe6866..37e26b47 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -1,75 +1,26 @@ package middleware -import ( - "strings" +//project-flock +const ( + P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" + P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" + P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" - "github.com/gofiber/fiber/v2" + P_ProjectFlockGetAll = "lti.production.project_flocks.list" + P_ProjectFlockCreate = "lti.production.project_flocks.create" + P_ProjectFlockGetOne = "lti.production.project_flocks.detail" + P_ProjectFlockUpdate = "lti.production.project_flocks.update" + P_ProjectFlockDelete = "lti.production.project_flocks.delete" + P_ProjectFlockApprove = "lti.production.project_flocks.approve" + P_ProjectFlockLookup = "lti.production.project_flocks.lookup" + P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period" + P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit" ) -// RequirePermissions ensures the authenticated user possesses all specified permissions. -func RequirePermissions(perms ...string) fiber.Handler { - required := canonicalPermissions(perms) - return func(c *fiber.Ctx) error { - if len(required) == 0 { - return c.Next() - } - - ctx, ok := AuthDetails(c) - if !ok || ctx == nil { - return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } - - userPerms := ctx.permissionSet() - if len(userPerms) == 0 { - return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") - } - - for _, perm := range required { - if _, has := userPerms[perm]; !has { - return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") - } - } - - return c.Next() - } -} - -// HasPermission reports whether the current request context includes the given permission. -func HasPermission(c *fiber.Ctx, perm string) bool { - ctx, ok := AuthDetails(c) - if !ok || ctx == nil { - return false - } - perm = canonicalPermission(perm) - if perm == "" { - return false - } - _, has := ctx.permissionSet()[perm] - return has -} - -func (a *AuthContext) permissionSet() map[string]struct{} { - if a == nil || a.Permissions == nil { - return nil - } - return a.Permissions -} - -func canonicalPermissions(perms []string) []string { - out := make([]string, 0, len(perms)) - seen := make(map[string]struct{}, len(perms)) - for _, perm := range perms { - if canonical := canonicalPermission(perm); canonical != "" { - if _, ok := seen[canonical]; ok { - continue - } - seen[canonical] = struct{}{} - out = append(out, canonical) - } - } - return out -} - -func canonicalPermission(perm string) string { - return strings.ToLower(strings.TrimSpace(perm)) -} +//recording +const ( + PermissionRecordingRead = "recording.index" + PermissionRecordingCreate = "recording.create" + PermissionRecordingUpdate = "recording.update" + PermissionRecordingDelete = "recording.delete" +) \ No newline at end of file diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index 7bab770e..d4dfec30 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -20,7 +20,7 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) } diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 710f5225..a962fd56 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -15,14 +15,14 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route := v1.Group("/project-flocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) - route.Post("/approvals", ctrl.Approval) - route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary) - route.Put("/:id/resubmit", ctrl.Resubmit) + route.Get("/",m.RequirePermissions(m.P_ProjectFlockGetAll),ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProjectFlockCreate), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ProjectFlockUpdate), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProjectFlockGetAll), ctrl.DeleteOne) + route.Get("/kandangs/lookup",m.RequirePermissions(m.P_ProjectFlockLookup), ctrl.LookupProjectFlockKandang) + route.Post("/approvals",m.RequirePermissions(m.P_ProjectFlockApprove), ctrl.Approval) + route.Get("/locations/:location_id/periods",m.RequirePermissions(m.P_ProjectFlockNextPeriod), ctrl.GetPeriodSummary) + route.Put("/:id/resubmit",m.RequirePermissions(m.P_ProjectFlockResubmit), ctrl.Resubmit) } diff --git a/internal/modules/users/route.go b/internal/modules/users/route.go index 9ba6bfb3..1093312f 100644 --- a/internal/modules/users/route.go +++ b/internal/modules/users/route.go @@ -3,7 +3,7 @@ package users import ( "github.com/gofiber/fiber/v2" - "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/users/controllers" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -12,11 +12,11 @@ func UserRoutes(v1 fiber.Router, s user.UserService) { ctrl := controller.NewUserController(s) route := v1.Group("/users") - route.Use(middleware.Auth(s)) + route.Use(m.Auth(s)) - route.Get("/", ctrl.GetAll) + route.Get("/", m.RequirePermissions("lti.users.list"), ctrl.GetAll) // route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Get("/:id", m.RequirePermissions("lti.users.detail"), ctrl.GetOne) // route.Patch("/:id", ctrl.UpdateOne) // route.Delete("/:id", ctrl.DeleteOne) } From 2effa0864880504dd1c69a8cd662757fba455bd2 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 10 Dec 2025 08:53:09 +0700 Subject: [PATCH 02/31] feat/BE/US-304/TASK-307,306-adjustment middleware check if user have permission,create all permission in modules lti --- internal/middleware/permissions.go | 165 +++++++++++++++++- internal/modules/approvals/route.go | 2 +- internal/modules/closings/route.go | 10 +- internal/modules/constants/route.go | 1 - internal/modules/expenses/route.go | 24 +-- .../modules/inventory/adjustments/route.go | 10 +- .../modules/inventory/product-stocks/route.go | 8 +- .../inventory/product-warehouses/route.go | 4 +- internal/modules/inventory/transfers/route.go | 6 +- .../marketing/delivery-orderss/route.go | 11 +- .../modules/marketing/sales-orders/route.go | 11 +- internal/modules/master/areas/route.go | 10 +- internal/modules/master/banks/route.go | 11 +- internal/modules/master/customers/route.go | 10 +- internal/modules/master/fcrs/route.go | 10 +- internal/modules/master/flocks/route.go | 10 +- internal/modules/master/kandangs/route.go | 10 +- internal/modules/master/locations/route.go | 10 +- internal/modules/master/nonstocks/route.go | 10 +- .../master/product-categories/route.go | 10 +- internal/modules/master/products/route.go | 10 +- internal/modules/master/suppliers/route.go | 10 +- internal/modules/master/uoms/route.go | 6 + internal/modules/master/warehouses/route.go | 10 +- internal/modules/production/chickins/route.go | 6 +- .../project-flock-kandangs/route.go | 6 - .../modules/production/recordings/route.go | 14 +- internal/modules/purchases/route.go | 16 +- internal/modules/users/route.go | 4 +- 29 files changed, 289 insertions(+), 136 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 37e26b47..0734b035 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -17,10 +17,167 @@ const ( P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit" ) +const( + P_ExpenseGetAll= "lti.expense.list" + P_ExpenseCreateOne= "lti.expense.create" + P_ExpenseUpdateOne= "lti.expense.update" + P_ExpenseGetOne= "lti.expense.detail" + P_ExpenseDeleteOne= "lti.expense.delete" + P_ExpenseApprovalManager= "lti.expense.approve.manager" + P_ExpenseApprovalFinance= "lti.expense.approve.finance" + P_ExpenseCreateRealizations= "lti.expense.create.realization" + P_ExpenseUpdateRealizations= "lti.expense.update.realization" + P_ExpenseCompleteExpense= "lti.expense.complete.expense" + P_ExpenseDocument= "lti.expense.document" + P_ExpenseDocumentRealizations= "lti.expense.document.realization" +) +const( + P_AdjustmentGetAll="lti.inventory.list" + P_AdjustmentCreate="lti.inventory.create" + P_AdjustmentGetOne="lti.inventory.detail" +) +const( + P_ApprovalGetAll = "lti.approval.list" +) + +const( + P_ClosingGetAll = "lti.closing.list" + P_ClosingPenjualan = "lti.closing.penjualan" + P_ClosingGetSummary = "lti.closing.getsummary" + P_ProductStockGetAll = "lti.inventory.product_stock.list" + P_ProductStockGetOne = "lti.inventory.product_stock.detail" + P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list" + P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" +) + +const( + P_TransferGetAll = "lti.inventory.transfer.list" + P_TransferGetOne = "lti.inventory.transfer.detail" + P_TransferCreateOne = "lti.inventory.transfer.create" +) + +const( + P_DeliveryGetAll = "lti.marketing.delivery_order.list" + P_DeliveryGetOne = "lti.marketing.delivery_order.detail" + P_DeliveryCreateOne = "lti.marketing.delivery_order.create" + P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" + P_SalesOrderDelete = "lti.marketing.sales_order.delete" + P_SalesOrderApproval = "lti.marketing.sales_order.approve" + P_SalesOrderCreateOne = "lti.marketing.sales_order.create" + P_SalesOrderUpdateOne = "lti.marketing.sales_order.update" +) + +const( + P_AreaGetAll = "lti.master.area.list" + P_AreaGetOne = "lti.master.area.detail" + P_AreaCreateOne = "lti.master.area.create" + P_AreaUpdateOne = "lti.master.area.update" + P_AreaDeleteOne = "lti.master.area.delete" + + P_BanksGetAll = "lti.master.banks.list" + P_BanksGetOne = "lti.master.banks.detail" + P_BanksCreateOne = "lti.master.banks.create" + P_BanksUpdateOne = "lti.master.banks.update" + P_BanksDeleteOne = "lti.master.banks.delete" + + P_CustomerGetAll = "lti.master.customer.list" + P_CustomerGetOne = "lti.master.customer.detail" + P_CustomerCreateOne = "lti.master.customer.create" + P_CustomerUpdateOne = "lti.master.customer.update" + P_CustomerDeleteOne = "lti.master.customer.delete" + + P_FcrGetAll = "lti.master.fcr.list" + P_FcrGetOne = "lti.master.fcr.detail" + P_FcrCreateOne = "lti.master.fcr.create" + P_FcrUpdateOne = "lti.master.fcr.update" + P_FcrDeleteOne = "lti.master.fcr.delete" + + P_FlocksGetAll = "lti.master.flocks.list" + P_FlocksGetOne = "lti.master.flocks.detail" + P_FlocksCreateOne = "lti.master.flocks.create" + P_FlocksUpdateOne = "lti.master.flocks.update" + P_FlocksDeleteOne = "lti.master.flocks.delete" + + P_KandangsGetAll = "lti.master.kandangs.list" + P_KandangsGetOne = "lti.master.kandangs.detail" + P_KandangsCreateOne = "lti.master.kandangs.create" + P_KandangsUpdateOne = "lti.master.kandangs.update" + P_KandangsDeleteOne = "lti.master.kandangs.delete" + + P_LocationsGetAll = "lti.master.locations.list" + P_LocationsGetOne = "lti.master.locations.detail" + P_LocationsCreateOne = "lti.master.locations.create" + P_LocationsUpdateOne = "lti.master.locations.update" + P_LocationsDeleteOne = "lti.master.locations.delete" + + P_NonstocksGetAll = "lti.master.nonstocks.list" + P_NonstocksGetOne = "lti.master.nonstocks.detail" + P_NonstocksCreateOne = "lti.master.nonstocks.create" + P_NonstocksUpdateOne = "lti.master.nonstocks.update" + P_NonstocksDeleteOne = "lti.master.nonstocks.delete" + + P_ProductCategoriesGetAll = "lti.master.Product_categories.list" + P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" + P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" + P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" + P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" + + P_ProductsGetAll = "lti.master.Products.list" + P_ProductsGetOne = "lti.master.Products.detail" + P_ProductsCreateOne = "lti.master.Products.create" + P_ProductsUpdateOne = "lti.master.Products.update" + P_ProductsDeleteOne = "lti.master.Products.delete" + + P_SuppliersGetAll = "lti.master.suppliers.list" + P_SuppliersGetOne = "lti.master.suppliers.detail" + P_SuppliersCreateOne = "lti.master.suppliers.create" + P_SuppliersUpdateOne = "lti.master.suppliers.update" + P_SuppliersDeleteOne = "lti.master.suppliers.delete" + + P_UomsGetAll = "lti.master.uoms.list" + P_UomsGetOne = "lti.master.uoms.detail" + P_UomsCreateOne = "lti.master.uoms.create" + P_UomsUpdateOne = "lti.master.uoms.update" + P_UomsDeleteOne = "lti.master.uoms.delete" + + P_WarehousesGetAll = "lti.master.warehouses.list" + P_WarehousesGetOne = "lti.master.warehouses.detail" + P_WarehousesCreateOne = "lti.master.warehouses.create" + P_WarehousesUpdateOne = "lti.master.warehouses.update" + P_WarehousesDeleteOne = "lti.master.warehouses.delete" + +) + + +const( + P_ChickinsCreateOne = "lti.production.chickins.create" + P_ChickinsGetOne = "lti.production.chickins.detail" + P_ChickinsApproval = "lti.production.chickins.approve" +) //recording const ( - PermissionRecordingRead = "recording.index" - PermissionRecordingCreate = "recording.create" - PermissionRecordingUpdate = "recording.update" - PermissionRecordingDelete = "recording.delete" + P_RecordingGetAll = "lti.production.recording.list" + P_RecordingGetOne = "lti.production.recording.detail" + P_RecordingCreateOne = "lti.production.recording.create" + P_RecordingUpdateOne = "lti.production.recording.update" + P_RecordingDeleteOne = "lti.production.recording.delete" + P_RecordingNextDay = "lti.production.recording.next_day" + P_RecordingApproval = "lti.production.recording.approve" +) + +const ( + P_PurchaseGetAll = "lti.Purchase.list" + P_PurchaseGetOne = "lti.Purchase.detail" + P_PurchaseCreateOne = "lti.Purchase.create" + P_PurchaseUpdateOne = "lti.Purchase.update" + P_PurchaseDeleteOne = "lti.Purchase.delete" + P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" + P_PurchaseReceive = "lti.Purchase.receive" + P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" + P_PurchaseApprovalManager = "lti.Purchase.approve.manager" +) + +const( + P_UserGetAll = "lti.users.list" + P_UserGetOne = "lti.users.detail" ) \ No newline at end of file diff --git a/internal/modules/approvals/route.go b/internal/modules/approvals/route.go index 5dd39616..cd479c03 100644 --- a/internal/modules/approvals/route.go +++ b/internal/modules/approvals/route.go @@ -15,5 +15,5 @@ func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalServic route := v1.Group("/approvals") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) + route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll)) } diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index ba18f3b9..059eb764 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -1,7 +1,7 @@ package closings import ( - // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers" closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,14 +13,14 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService ctrl := controller.NewClosingController(s) route := v1.Group("/closing") - + route.Use(m.Auth(u)) // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan) - route.Get("/:projectFlockId", ctrl.GetClosingSummary) + route.Get("/",m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) + route.Get("/:project_flock_id/penjualan",m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan) + route.Get("/:projectFlockId",m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary) } diff --git a/internal/modules/constants/route.go b/internal/modules/constants/route.go index 1da14371..46def610 100644 --- a/internal/modules/constants/route.go +++ b/internal/modules/constants/route.go @@ -12,6 +12,5 @@ func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) { ctrl := controller.NewConstantController(s) route := v1.Group("/constants") - route.Get("/", ctrl.GetAll) } diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index 1fc5c07a..fa3191fa 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -22,16 +22,16 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals/manager", ctrl.Approval) - route.Post("/approvals/finance", ctrl.Approval) - route.Post("/:id/realizations", ctrl.CreateRealization) - route.Patch("/:id/realizations", ctrl.UpdateRealization) - route.Post("/:id/complete", ctrl.CompleteExpense) - route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument) - route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument) + route.Get("/",m.RequirePermissions(m.P_ExpenseGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ExpenseCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne) + route.Post("/approvals/manager",m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval) + route.Post("/approvals/finance",m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval) + route.Post("/:id/realizations",m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization) + route.Patch("/:id/realizations",m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization) + route.Post("/:id/complete",m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense) + route.Delete("/:id/documents/:documentId",m.RequirePermissions(m.P_ExpenseDocument), ctrl.DeleteDocument) + route.Delete("/:id/realization-documents/:documentId",m.RequirePermissions(m.P_ExpenseDocumentRealizations), ctrl.DeleteRealizationDocument) } diff --git a/internal/modules/inventory/adjustments/route.go b/internal/modules/inventory/adjustments/route.go index 8f58bb4d..f99fe01e 100644 --- a/internal/modules/inventory/adjustments/route.go +++ b/internal/modules/inventory/adjustments/route.go @@ -1,7 +1,7 @@ package adjustments 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/inventory/adjustments/controllers" adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,10 +13,10 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme ctrl := controller.NewAdjustmentController(s) route := v1.Group("/adjustments") - + route.Use(m.Auth(u)) // Standard CRUD routes following master data pattern - route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters - route.Post("/", ctrl.Adjustment) // Create adjustment - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_AdjustmentGetAll), ctrl.AdjustmentHistory) // Get all with pagination and filters + route.Post("/",m.RequirePermissions(m.P_AdjustmentCreate), ctrl.Adjustment) // Create adjustment + route.Get("/:id",m.RequirePermissions(m.P_AdjustmentGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/product-stocks/route.go b/internal/modules/inventory/product-stocks/route.go index c7bb37f8..41714edc 100644 --- a/internal/modules/inventory/product-stocks/route.go +++ b/internal/modules/inventory/product-stocks/route.go @@ -1,7 +1,7 @@ package productStocks 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/inventory/product-stocks/controllers" productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,13 +13,13 @@ func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.Prod ctrl := controller.NewProductStockController(s) route := v1.Group("/product-stocks") - +route.Use(m.Auth(u)) // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProductStockGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProductStockGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/product-warehouses/route.go b/internal/modules/inventory/product-warehouses/route.go index 9c6c8e2b..81c06a08 100644 --- a/internal/modules/inventory/product-warehouses/route.go +++ b/internal/modules/inventory/product-warehouses/route.go @@ -15,7 +15,7 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho route := v1.Group("/product-warehouses") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProductWarehousekGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProductWarehouseGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/transfers/route.go b/internal/modules/inventory/transfers/route.go index f608af42..d24dbcb4 100644 --- a/internal/modules/inventory/transfers/route.go +++ b/internal/modules/inventory/transfers/route.go @@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ route := v1.Group("/transfers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) } diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go index c83330da..f4c08457 100644 --- a/internal/modules/marketing/delivery-orderss/route.go +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -11,13 +11,12 @@ import ( func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { ctrl := controller.NewDeliveryOrdersController(s) - - v1.Get("/", ctrl.GetAll) - v1.Get("/:id", ctrl.GetOne) + v1.Use(m.Auth(u)) + v1.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), ctrl.GetAll) + v1.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), ctrl.GetOne) // Sisanya di group /delivery-orders route := v1.Group("/delivery-orders") - route.Use(m.Auth(u)) // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) @@ -25,7 +24,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders. // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Post("/", ctrl.CreateOne) - route.Patch("/:id", ctrl.UpdateOne) + route.Post("/",m.RequirePermissions(m.P_DeliveryCreateOne), ctrl.CreateOne) + route.Patch("/:id",m.RequirePermissions(m.P_DeliveryUpdateOne), ctrl.UpdateOne) } diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index f87cea66..17249840 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -11,17 +11,16 @@ import ( func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { ctrl := controller.NewSalesOrdersController(s) - - v1.Delete("/:id", ctrl.DeleteOne) + v1.Use(m.Auth(u)) + v1.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), ctrl.DeleteOne) route := v1.Group("/sales-orders") - route.Use(m.Auth(u)) // route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Post("/", ctrl.CreateOne) - route.Patch("/:id", ctrl.UpdateOne) + route.Post("/",m.RequirePermissions(m.P_SalesOrderCreateOne), ctrl.CreateOne) + route.Patch("/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), ctrl.UpdateOne) - route.Post("/approvals", ctrl.Approval) + route.Post("/approvals",m.RequirePermissions(m.P_SalesOrderApproval), ctrl.Approval) } diff --git a/internal/modules/master/areas/route.go b/internal/modules/master/areas/route.go index 755a542e..0d715fb7 100644 --- a/internal/modules/master/areas/route.go +++ b/internal/modules/master/areas/route.go @@ -15,9 +15,9 @@ func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) { route := v1.Group("/areas") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/banks/route.go b/internal/modules/master/banks/route.go index 2e5bed3b..678a834c 100644 --- a/internal/modules/master/banks/route.go +++ b/internal/modules/master/banks/route.go @@ -14,10 +14,9 @@ func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) { route := v1.Group("/banks") route.Use(m.Auth(u)) - - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_BanksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_BanksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_BanksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_BanksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_BanksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/customers/route.go b/internal/modules/master/customers/route.go index d361e167..92f8139e 100644 --- a/internal/modules/master/customers/route.go +++ b/internal/modules/master/customers/route.go @@ -15,9 +15,9 @@ func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerServ route := v1.Group("/customers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_CustomerGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_CustomerCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_CustomerGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_CustomerUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_CustomerDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/fcrs/route.go b/internal/modules/master/fcrs/route.go index 60633f16..06291ce4 100644 --- a/internal/modules/master/fcrs/route.go +++ b/internal/modules/master/fcrs/route.go @@ -15,9 +15,9 @@ func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) { route := v1.Group("/fcrs") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_FcrGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_FcrCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_FcrGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_FcrUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_FcrDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/flocks/route.go b/internal/modules/master/flocks/route.go index 429d8dcd..046e014a 100644 --- a/internal/modules/master/flocks/route.go +++ b/internal/modules/master/flocks/route.go @@ -15,9 +15,9 @@ func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) { route := v1.Group("/flocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_FlocksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_FlocksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_FlocksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_FlocksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_FlocksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/kandangs/route.go b/internal/modules/master/kandangs/route.go index 6a425b64..4cbf2793 100644 --- a/internal/modules/master/kandangs/route.go +++ b/internal/modules/master/kandangs/route.go @@ -15,9 +15,9 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService route := v1.Group("/kandangs") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_KandangsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_KandangsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_KandangsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_KandangsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_KandangsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/locations/route.go b/internal/modules/master/locations/route.go index 68bce594..771e2d0d 100644 --- a/internal/modules/master/locations/route.go +++ b/internal/modules/master/locations/route.go @@ -15,9 +15,9 @@ func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationServ route := v1.Group("/locations") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_LocationsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_LocationsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_LocationsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_LocationsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_LocationsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/nonstocks/route.go b/internal/modules/master/nonstocks/route.go index 2aa7b838..6f2a2016 100644 --- a/internal/modules/master/nonstocks/route.go +++ b/internal/modules/master/nonstocks/route.go @@ -15,9 +15,9 @@ func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockServ route := v1.Group("/nonstocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_NonstocksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_NonstocksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_NonstocksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_NonstocksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_NonstocksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/product-categories/route.go b/internal/modules/master/product-categories/route.go index 4a2262f9..1fa0532f 100644 --- a/internal/modules/master/product-categories/route.go +++ b/internal/modules/master/product-categories/route.go @@ -15,9 +15,9 @@ func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategor route := v1.Group("/product-categories") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_ProductCategoriesGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProductCategoriesCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProductCategoriesGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ProductCategoriesUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProductCategoriesDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/products/route.go b/internal/modules/master/products/route.go index 369d6ea8..04431bd4 100644 --- a/internal/modules/master/products/route.go +++ b/internal/modules/master/products/route.go @@ -15,9 +15,9 @@ func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService route := v1.Group("/products") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_ProductsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProductsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProductsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ProductsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProductsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/suppliers/route.go b/internal/modules/master/suppliers/route.go index 17271d4a..564ac725 100644 --- a/internal/modules/master/suppliers/route.go +++ b/internal/modules/master/suppliers/route.go @@ -15,9 +15,9 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ route := v1.Group("/suppliers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_SuppliersGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_SuppliersCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_SuppliersGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_SuppliersUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_SuppliersDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/uoms/route.go b/internal/modules/master/uoms/route.go index 53faa239..8ffbcb62 100644 --- a/internal/modules/master/uoms/route.go +++ b/internal/modules/master/uoms/route.go @@ -20,4 +20,10 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) { route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + + route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/warehouses/route.go b/internal/modules/master/warehouses/route.go index 8acf4452..a08b04a5 100644 --- a/internal/modules/master/warehouses/route.go +++ b/internal/modules/master/warehouses/route.go @@ -15,9 +15,9 @@ func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseS route := v1.Group("/warehouses") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_WarehousesGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_WarehousesCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_WarehousesGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_WarehousesUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_WarehousesDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index a558dd29..103a3655 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -16,9 +16,9 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService route.Use(m.Auth(u)) // route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Post("/",m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne) // route.Patch("/:id", ctrl.UpdateOne) // route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) + route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval) } diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index d4dfec30..b382d1af 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -14,12 +14,6 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo route := v1.Group("/project-flock-kandangs") route.Use(m.Auth(u)) - // route.Get("/", m.Auth(u), ctrl.GetAll) - // route.Post("/", m.Auth(u), ctrl.CreateOne) - // route.Get("/:id", m.Auth(u), ctrl.GetOne) - // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) - // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll) route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) diff --git a/internal/modules/production/recordings/route.go b/internal/modules/production/recordings/route.go index 83b426db..f05d054d 100644 --- a/internal/modules/production/recordings/route.go +++ b/internal/modules/production/recordings/route.go @@ -15,11 +15,11 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS route := v1.Group("/recordings") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Get("/next-day", ctrl.GetNextDay) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Post("/approvals", ctrl.Approve) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne) + route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne) + route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne) + route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay) + route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve) } diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index 5145bc94..4be485e6 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -15,12 +15,12 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe route := router.Group("/purchases") route.Use(m.Auth(userService)) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) - route.Post("/", ctrl.CreateOne) - route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) - route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase) - route.Post("/:id/receipts", ctrl.ReceiveProducts) - route.Delete("/:id", ctrl.DeletePurchase) - route.Delete("/:id/items", ctrl.DeleteItems) + route.Get("/",m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne) + route.Post("/",m.RequirePermissions(m.P_PurchaseCreateOne), ctrl.CreateOne) + route.Post("/:id/approvals/staff",m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase) + route.Post("/:id/approvals/manager",m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase) + route.Post("/:id/receipts",m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts) + route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeletePurchase) + route.Delete("/:id/items",m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems) } diff --git a/internal/modules/users/route.go b/internal/modules/users/route.go index 1093312f..d6aa03fe 100644 --- a/internal/modules/users/route.go +++ b/internal/modules/users/route.go @@ -14,9 +14,9 @@ func UserRoutes(v1 fiber.Router, s user.UserService) { route := v1.Group("/users") route.Use(m.Auth(s)) - route.Get("/", m.RequirePermissions("lti.users.list"), ctrl.GetAll) + route.Get("/", m.RequirePermissions(m.P_UserGetAll), ctrl.GetAll) // route.Post("/", ctrl.CreateOne) - route.Get("/:id", m.RequirePermissions("lti.users.detail"), ctrl.GetOne) + route.Get("/:id", m.RequirePermissions(m.P_UserGetOne), ctrl.GetOne) // route.Patch("/:id", ctrl.UpdateOne) // route.Delete("/:id", ctrl.DeleteOne) } From fc49cef781f82d019821a181eaae9ebe60038f03 Mon Sep 17 00:00:00 2001 From: ragilap Date: Sun, 14 Dec 2025 23:15:30 +0700 Subject: [PATCH 03/31] add counting hpp-expedition by project --- .../controllers/closing.controller.go | 32 ++++++++++++ .../closings/dto/closingExpedition.dto.go | 18 +++++++ .../repositories/closing.repository.go | 49 +++++++++++++++++++ internal/modules/closings/route.go | 1 + .../closings/services/closing.service.go | 43 ++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 internal/modules/closings/dto/closingExpedition.dto.go diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index dc39a666..1f61a775 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -188,3 +188,35 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { Data: result, }) } + +func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + } + + var projectFlockKandangID *uint + if raw := c.Query("project_flock_kandang_id"); raw != "" { + idInt, convErr := strconv.Atoi(raw) + if convErr != nil || idInt <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + idUint := uint(idInt) + projectFlockKandangID = &idUint + } + + result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expedition HPP successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closingExpedition.dto.go b/internal/modules/closings/dto/closingExpedition.dto.go new file mode 100644 index 00000000..f1b8628b --- /dev/null +++ b/internal/modules/closings/dto/closingExpedition.dto.go @@ -0,0 +1,18 @@ +package dto + +// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor. +type ExpeditionCostItemDTO struct { + Id uint64 `json:"id"` + ExpeditionVendorID uint64 `json:"expedition_vendor_id"` + ExpeditionVendorName string `json:"expedition_vendor_name"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + HPPAmount float64 `json:"hpp_amount"` +} + +// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi. +type ExpeditionHPPDTO struct { + ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"` + TotalHPPAmount float64 `json:"total_hpp_amount"` +} + diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index fe555378..ecdfd125 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -9,12 +9,14 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) + GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) } type ClosingRepositoryImpl struct { @@ -44,6 +46,13 @@ type SapronakRow struct { Notes string `gorm:"column:notes"` } +type ExpeditionHPPRow struct { + SupplierID uint64 `gorm:"column:supplier_id"` + SupplierName string `gorm:"column:supplier_name"` + Qty float64 `gorm:"column:qty"` + TotalAmount float64 `gorm:"column:total_amount"` +} + type SapronakQueryParams struct { Type string WarehouseIDs []uint @@ -102,6 +111,46 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak return rows, totalResults, nil } +func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { + db := r.DB().WithContext(ctx) + + if projectFlockID == 0 { + return nil, fmt.Errorf("invalid project flock id") + } + + query := db. + Table("expense_realizations AS er"). + Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id"). + Joins("JOIN expenses e ON e.id = ens.expense_id"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id"). + Joins("JOIN nonstocks n ON n.id = ens.nonstock_id"). + Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock). + Joins("JOIN suppliers s ON s.id = e.supplier_id"). + Where("pfk.project_flock_id = ?", projectFlockID). + Where("e.category = ?", "BOP"). + Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) + + if projectFlockKandangID != nil && *projectFlockKandangID != 0 { + query = query.Where("pfk.id = ?", *projectFlockKandangID) + } + + var rows []ExpeditionHPPRow + err := query. + Select( + "e.supplier_id AS supplier_id, " + + "s.name AS supplier_name, " + + "SUM(er.qty) AS qty, " + + "SUM(er.qty * er.price) AS total_amount", + ). + Group("e.supplier_id, s.name"). + Scan(&rows).Error + if err != nil { + return nil, err + } + + return rows, nil +} + const ( sapronakIncomingPurchasesSQL = ` SELECT diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 4d142f44..6a35ba06 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,4 +25,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + route.Get("/:project_flock_id/expedition-hpp", ctrl.GetExpeditionHPP) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index cfc22948..12357e46 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -31,6 +31,7 @@ type ClosingService interface { GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) + GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) } type closingService struct { @@ -379,3 +380,45 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove return &result, nil } + +// GetExpeditionHPP menghitung HPP ekspedisi per vendor untuk sebuah project flock. +// Jika projectFlockKandangID tidak nil, maka hanya data untuk kandang tersebut yang dihitung. +func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP") + } + + expeditionCosts := make([]dto.ExpeditionCostItemDTO, 0, len(rows)) + var totalHPP float64 + + for idx, row := range rows { + unitPrice := 0.0 + if row.Qty > 0 { + unitPrice = row.TotalAmount / row.Qty + } + + expeditionCosts = append(expeditionCosts, dto.ExpeditionCostItemDTO{ + Id: uint64(idx + 1), + ExpeditionVendorID: row.SupplierID, + ExpeditionVendorName: row.SupplierName, + Qty: row.Qty, + UnitPrice: unitPrice, + HPPAmount: row.TotalAmount, + }) + + totalHPP += row.TotalAmount + } + + result := &dto.ExpeditionHPPDTO{ + ExpeditionCosts: expeditionCosts, + TotalHPPAmount: totalHPP, + } + + return result, nil +} From cd739f41b94def0dbc651816711ea056e5d00590 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 16 Dec 2025 14:42:31 +0700 Subject: [PATCH 04/31] Feat(BE-339): make a report for purchasing supplier --- .../controllers/repport.controller.go | 50 +++++ .../repports/dto/repportPurchase.dto.go | 138 ++++++++++++ internal/modules/repports/module.go | 4 +- .../purchase_supplier.repository.go | 196 ++++++++++++++++++ internal/modules/repports/route.go | 1 + .../repports/services/repport.service.go | 69 +++++- .../validations/repport.validation.go | 13 ++ internal/response/response.go | 9 +- 8 files changed, 474 insertions(+), 6 deletions(-) create mode 100644 internal/modules/repports/dto/repportPurchase.dto.go create mode 100644 internal/modules/repports/repositories/purchase_supplier.repository.go diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..3e6c39d0 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -97,3 +97,53 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { Data: result, }) } + +func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { + query := &validation.PurchaseSupplierQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + AreaId: int64(ctx.QueryInt("area_id", 0)), + SupplierId: int64(ctx.QueryInt("supplier_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)), + DateFrom: ctx.Query("date_from", ""), + DateTo: ctx.Query("date_to", ""), + SortBy: ctx.Query("sort_by", ""), + FilterBy: ctx.Query("filter_by", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetPurchaseSupplier(ctx, query) + if err != nil { + return err + } + + filters := map[string]interface{}{ + "area_id": query.AreaId, + "supplier_id": query.SupplierId, + "product_id": query.ProductId, + "product_category_id": query.ProductCategoryId, + "date_from": query.DateFrom, + "date_to": query.DateTo, + "sort_by": query.SortBy, + "filter_by": query.FilterBy, + } + + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.PurchaseSupplierDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get supplier purchase recap 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, + }) +} diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go new file mode 100644 index 00000000..60fd0fee --- /dev/null +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -0,0 +1,138 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" +) + +type PurchaseSupplierRowDTO struct { + ReceiveDate string `json:"receive_date"` + PoDate string `json:"po_date"` + PoNumber string `json:"po_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + PurchaseValue float64 `json:"purchase_value"` + TransportUnitPrice float64 `json:"transport_unit_price"` + TransportValue float64 `json:"transport_value"` + TotalAmount float64 `json:"total_amount"` + Expedition string `json:"expedition"` + DeliveryNumber string `json:"delivery_number"` +} + +type PurchaseSupplierSummaryDTO struct { + TotalQty float64 `json:"total_qty"` + TotalPurchaseValue float64 `json:"total_purchase_value"` + TotalTransportValue float64 `json:"total_transport_value"` + TotalAmount float64 `json:"total_amount"` +} + +type PurchaseSupplierDTO struct { + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + Rows []PurchaseSupplierRowDTO `json:"rows"` + Summary PurchaseSupplierSummaryDTO `json:"summary"` +} + +func formatDatePtr(t *time.Time) string { + if t == nil || t.IsZero() { + return "" + } + return t.Format("02-Jan-2006") +} + +func ToPurchaseSupplierRowDTO(item *entity.PurchaseItem) PurchaseSupplierRowDTO { + row := PurchaseSupplierRowDTO{ + ReceiveDate: formatDatePtr(item.ReceivedDate), + Qty: item.TotalQty, + UnitPrice: item.Price, + } + + if item.Purchase != nil { + row.PoDate = formatDatePtr(item.Purchase.PoDate) + if item.Purchase.PoNumber != nil { + row.PoNumber = *item.Purchase.PoNumber + } + } + + if item.Product != nil && item.Product.Id != 0 { + product := productDTO.ToProductRelationDTO(*item.Product) + row.Product = &product + } + + if item.Warehouse != nil && item.Warehouse.Id != 0 { + warehouse := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse) + row.Warehouse = &warehouse + } + + qty := row.Qty + if qty < 0 { + qty = 0 + } + + row.PurchaseValue = row.UnitPrice * qty + + var transportUnit float64 + var expeditionName string + + if item.ExpenseNonstock != nil { + transportUnit = item.ExpenseNonstock.Price + + if item.ExpenseNonstock.Expense != nil && + item.ExpenseNonstock.Expense.Supplier != nil && + item.ExpenseNonstock.Expense.Supplier.Id != 0 { + expSupplier := item.ExpenseNonstock.Expense.Supplier + expeditionName = expSupplier.Name + } + } + + row.TransportUnitPrice = transportUnit + row.TransportValue = transportUnit * qty + row.TotalAmount = row.PurchaseValue + row.TransportValue + + if expeditionName == "" { + row.Expedition = "-" + } else { + row.Expedition = expeditionName + } + + if item.TravelNumber != nil && *item.TravelNumber != "" { + row.DeliveryNumber = *item.TravelNumber + } else { + row.DeliveryNumber = "-" + } + + return row +} + +func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem) PurchaseSupplierDTO { + var supplierDTORef *supplierDTO.SupplierRelationDTO + if supplier.Id != 0 { + mapped := supplierDTO.ToSupplierRelationDTO(supplier) + supplierDTORef = &mapped + } + + rows := make([]PurchaseSupplierRowDTO, 0, len(items)) + summary := PurchaseSupplierSummaryDTO{} + + for i := range items { + row := ToPurchaseSupplierRowDTO(&items[i]) + rows = append(rows, row) + + summary.TotalQty += row.Qty + summary.TotalPurchaseValue += row.PurchaseValue + summary.TotalTransportValue += row.TransportValue + summary.TotalAmount += row.TotalAmount + } + + return PurchaseSupplierDTO{ + Supplier: supplierDTORef, + Rows: rows, + Summary: summary, + } +} + diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..f3798f6a 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -7,6 +7,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" @@ -20,9 +21,10 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) + purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc, purchaseSupplierRepository) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go new file mode 100644 index 00000000..cd282e8e --- /dev/null +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -0,0 +1,196 @@ +package repositories + +import ( + "context" + "fmt" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "gorm.io/gorm" +) + +type PurchaseSupplierRepository interface { + GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) + GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) +} + +type purchaseSupplierRepositoryImpl struct { + db *gorm.DB +} + +func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { + return &purchaseSupplierRepositoryImpl{db: db} +} + +func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { + // Tentukan kolom tanggal yang akan dipakai untuk filter + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + db := r.db.WithContext(ctx). + Model(&entity.Supplier{}). + Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + + if filters.SupplierId > 0 { + db = db.Where("suppliers.id = ?", filters.SupplierId) + } + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.DateFrom != "" { + if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.DateTo != "" { + if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + return db +} + +func (r *purchaseSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) { + query := r.baseSupplierQuery(ctx, filters) + + var totalSuppliers int64 + if err := query. + Distinct("suppliers.id"). + Count(&totalSuppliers).Error; err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 { + return []entity.Supplier{}, 0, nil + } + + if offset < 0 { + offset = 0 + } + + var supplierIDs []uint + if err := query. + Select("suppliers.id"). + Order("suppliers.id ASC"). + Offset(offset). + Limit(limit). + Pluck("suppliers.id", &supplierIDs).Error; err != nil { + return nil, 0, err + } + + if len(supplierIDs) == 0 { + return []entity.Supplier{}, totalSuppliers, nil + } + + var suppliers []entity.Supplier + if err := r.db.WithContext(ctx). + Where("id IN ?", supplierIDs). + Find(&suppliers).Error; err != nil { + return nil, 0, err + } + + return suppliers, totalSuppliers, nil +} + +func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) { + if len(supplierIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + // Tentukan kolom tanggal yang akan dipakai untuk filter & sort + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + orderDirection := "ASC" + switch strings.ToUpper(strings.TrimSpace(filters.SortBy)) { + case "DESC": + orderDirection = "DESC" + case "ASC", "": + orderDirection = "ASC" + } + + db := r.db.WithContext(ctx). + Model(&entity.PurchaseItem{}). + Preload("Purchase"). + Preload("Purchase.Supplier"). + Preload("Product"). + Preload("Product.ProductCategory"). + Preload("Warehouse"). + Preload("Warehouse.Area"). + Preload("Warehouse.Location"). + Preload("Warehouse.Kandang"). + Preload("ExpenseNonstock"). + Preload("ExpenseNonstock.Expense"). + Preload("ExpenseNonstock.Expense.Supplier"). + Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). + Where("purchases.supplier_id IN ?", supplierIDs) + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.DateFrom != "" { + if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.DateTo != "" { + if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + // Urutkan berdasarkan kolom tanggal yang dipilih dan arah sort + db = db.Order(fmt.Sprintf("%s %s", dateColumn, orderDirection)). + Order("purchase_items.id ASC") + + var items []entity.PurchaseItem + if err := db.Find(&items).Error; err != nil { + return nil, err + } + + return items, nil +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 4aea831c..d24caac5 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -14,4 +14,5 @@ func RepportRoutes(v1 fiber.Router, s repport.RepportService) { route.Get("/expense", ctrl.GetExpense) route.Get("/marketing", ctrl.GetMarketing) + route.Get("/purchase-supplier", ctrl.GetPurchaseSupplier) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..aa649871 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -2,6 +2,7 @@ package service import ( "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -10,6 +11,8 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" @@ -19,6 +22,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) } type repportService struct { @@ -27,15 +31,23 @@ type repportService struct { ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository ApprovalSvc approvalService.ApprovalService + PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService( + validate *validator.Validate, + expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, + marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, + approvalSvc approvalService.ApprovalService, + purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, +) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, ApprovalSvc: approvalSvc, + PurchaseSupplierRepo: purchaseSupplierRepo, } } @@ -113,3 +125,58 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil } + +func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + if offset < 0 { + offset = 0 + } + + suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) + if err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 || len(suppliers) == 0 { + return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil + } + + supplierMap := make(map[uint]entity.Supplier, len(suppliers)) + supplierIDs := make([]uint, 0, len(suppliers)) + for _, supplier := range suppliers { + supplierMap[supplier.Id] = supplier + supplierIDs = append(supplierIDs, supplier.Id) + } + + items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params) + if err != nil { + return nil, 0, err + } + + itemsBySupplier := make(map[uint][]entity.PurchaseItem) + for _, item := range items { + if item.Purchase == nil { + continue + } + supplierID := item.Purchase.SupplierId + itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item) + } + + result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs)) + for _, supplierID := range supplierIDs { + supplier, exists := supplierMap[supplierID] + if !exists { + continue + } + + supplierItems := itemsBySupplier[supplierID] + dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems) + result = append(result, dtoItem) + } + + return result, totalSuppliers, nil +} diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..942eeaa8 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -27,3 +27,16 @@ type MarketingQuery struct { SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` MarketingId int64 `query:"marketing_id" validate:"omitempty"` } + +type PurchaseSupplierQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + AreaId int64 `query:"area_id" validate:"omitempty"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` + DateFrom string `query:"date_from" validate:"omitempty"` + DateTo string `query:"date_to" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty"` +} diff --git a/internal/response/response.go b/internal/response/response.go index c4ecca0f..710d320e 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -14,10 +14,11 @@ type Success struct { } type Meta struct { - Page int `json:"page"` - Limit int `json:"limit"` - TotalPages int64 `json:"total_pages"` - TotalResults int64 `json:"total_results"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int64 `json:"total_pages"` + TotalResults int64 `json:"total_results"` + Filters interface{} `json:"filters,omitempty"` } type SuccessWithPaginate[T any] struct { From afe4b2ffe395ae05d42c436a7a139a632213aaf8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 16 Dec 2025 21:10:48 +0700 Subject: [PATCH 05/31] feat[BE}: change get penjualan repport dto an add more params --- .../controllers/closing.controller.go | 22 ++ .../closings/dto/closingKeuangan.dto.go | 186 +++++++++++++ internal/modules/closings/route.go | 2 + .../closings/services/closing.service.go | 235 ++++++++++++++++ .../expense_realization.repository.go | 6 +- .../salesorder_delivery_product.repository.go | 119 ++++++-- .../controllers/repport.controller.go | 24 +- .../repports/dto/repportMarketing.dto.go | 260 ++++++------------ .../repports/services/repport.service.go | 112 ++++++-- .../validations/repport.validation.go | 22 +- 10 files changed, 741 insertions(+), 247 deletions(-) create mode 100644 internal/modules/closings/dto/closingKeuangan.dto.go diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index a04fc5f9..bca2f9cb 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -245,3 +245,25 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { Data: payload, }) } + +func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + } + + result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get closing keuangan successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go new file mode 100644 index 00000000..d380dc3d --- /dev/null +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -0,0 +1,186 @@ +package dto + +// === BASE METRICS === +type FinancialMetrics struct { + RpPerBird float64 `json:"rp_per_bird"` + RpPerKg float64 `json:"rp_per_kg"` + Amount float64 `json:"amount"` +} + +type Comparison struct { + Budgeting FinancialMetrics `json:"budgeting"` + Realization FinancialMetrics `json:"realization"` +} + +// === HPP PURCHASES PACKAGE === +type HppItem struct { + Type string `json:"type"` + Comparison +} + +type HppGroup struct { + GroupName string `json:"group_name"` + Data []HppItem `json:"data"` +} + +type SummaryHpp struct { + Label string `json:"label"` + Comparison +} + +// Ini adalah struct mandiri untuk bagian HPP Purchases +type HppPurchasesSection struct { + Title string `json:"title"` + Hpp []HppGroup `json:"hpp"` + SummaryHpp SummaryHpp `json:"summary_hpp"` +} + +// === PROFIT LOSS PACKAGE === +type PLItem struct { + Type string `json:"type"` + FinancialMetrics +} + +type PLSummaryItem struct { + Label string `json:"label"` + FinancialMetrics +} + +type PLSummaryGroup struct { + GrossProfit PLSummaryItem `json:"gross_profit"` + SubTotal PLSummaryItem `json:"sub_total"` + NetProfit PLSummaryItem `json:"net_profit"` +} + +type ProfitLossData struct { + Penjualan []PLItem `json:"penjualan"` + Pembelian []PLItem `json:"pembelian"` + Summary PLSummaryGroup `json:"summary"` +} + +// Ini adalah struct mandiri untuk bagian Profit Loss +type ProfitLossSection struct { + Title string `json:"title"` + Data ProfitLossData `json:"data"` +} + +// === RESPONSE DTO (ROOT) === +// Sekarang Root-nya terlihat sangat bersih dan tidak "janggal" lagi +type ReportResponse struct { + HppPurchases HppPurchasesSection `json:"hpp_purchases"` + ProfitLoss ProfitLossSection `json:"profit_loss"` +} + +// === MAPPER FUNCTIONS === + +// FinancialMetrics Mappers +func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { + return FinancialMetrics{ + RpPerBird: rpPerBird, + RpPerKg: rpPerKg, + Amount: amount, + } +} + +// Comparison Mappers +func ToComparison(budgeting, realization FinancialMetrics) Comparison { + return Comparison{ + Budgeting: budgeting, + Realization: realization, + } +} + +// HppItem Mappers +func ToHppItem(itemType string, comparison Comparison) HppItem { + return HppItem{ + Type: itemType, + Comparison: comparison, + } +} + +// HppGroup Mappers +func ToHppGroup(groupName string, items []HppItem) HppGroup { + return HppGroup{ + GroupName: groupName, + Data: items, + } +} + +// SummaryHpp Mappers +func ToSummaryHpp(label string, comparison Comparison) SummaryHpp { + return SummaryHpp{ + Label: label, + Comparison: comparison, + } +} + +// HppPurchasesSection Mappers +func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) HppPurchasesSection { + return HppPurchasesSection{ + Title: title, + Hpp: hppGroups, + SummaryHpp: summaryHpp, + } +} + +// PLItem Mappers +func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { + return PLItem{ + Type: itemType, + FinancialMetrics: metrics, + } +} + +// PLSummaryItem Mappers +func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { + return PLSummaryItem{ + Label: label, + FinancialMetrics: metrics, + } +} + +// PLSummaryGroup Mappers +func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { + return PLSummaryGroup{ + GrossProfit: grossProfit, + SubTotal: subTotal, + NetProfit: netProfit, + } +} + +// ProfitLossData Mappers +func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { + return ProfitLossData{ + Penjualan: penjualan, + Pembelian: pembelian, + Summary: summary, + } +} + +// ProfitLossSection Mappers +func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection { + return ProfitLossSection{ + Title: title, + Data: data, + } +} + +// ReportResponse Mappers +func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { + return ReportResponse{ + HppPurchases: hppPurchases, + ProfitLoss: profitLoss, + } +} + +// Helper function to create a complete financial report +func BuildFinancialReport( + hppGroups []HppGroup, + summaryHpp SummaryHpp, + penjualan, pembelian []PLItem, + plSummary PLSummaryGroup, +) ReportResponse { + hppSection := ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) + plSection := ToProfitLossSection("Laporan Laba Rugi", ToProfitLossData(penjualan, pembelian, plSummary)) + return ToReportResponse(hppSection, plSection) +} diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index a76e8b79..62998f2c 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,6 +25,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject) + route.Get("/:project_flock_id/keuangan", ctrl.GetClosingKeuangan) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index b1780359..e6e74d45 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -31,6 +31,7 @@ type ClosingService interface { GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) + GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) } type closingService struct { @@ -379,3 +380,237 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove 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") + } + + _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + if err != nil { + s.Log.Errorf("Failed to get project flock %d for closing keuangan: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get budgets for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get realizations for project flock %d: %+v", projectFlockID, err) + 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) { + s.Log.Errorf("Failed to get delivery products for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") + } + + chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get chickins for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") + } + + var totalPopulation float64 + for _, chickin := range chickins { + totalPopulation += chickin.UsageQty + } + + var totalWeightSold float64 + for _, delivery := range deliveryProducts { + totalWeightSold += delivery.TotalWeight + } + + hppItems := s.buildHppItems(budgets, realizations, totalWeightSold, totalPopulation) + hppGroups := []dto.HppGroup{ + dto.ToHppGroup("Input Produksi", hppItems), + } + + summaryHpp := s.calculateHppSummary(budgets, realizations, totalWeightSold, totalPopulation) + + penjualanItems := s.buildPenjualanItems(deliveryProducts, totalPopulation, totalWeightSold) + pembelianItems := s.buildPembelianItems(budgets, realizations, totalPopulation, totalWeightSold) + plSummary := s.calculatePLSummary(penjualanItems, pembelianItems) + + hppSection := dto.ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) + plSection := dto.ToProfitLossSection("Laporan Laba Rugi", dto.ToProfitLossData(penjualanItems, pembelianItems, plSummary)) + + report := dto.ToReportResponse(hppSection, plSection) + + return &report, nil +} + +func (s closingService) buildHppItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) []dto.HppItem { + var totalBudgetAmount float64 + var totalRealizationAmount float64 + + for _, budget := range budgets { + totalBudgetAmount += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealizationAmount += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudgetAmount / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudgetAmount / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealizationAmount / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealizationAmount / totalWeightSold + } + + items := []dto.HppItem{ + dto.ToHppItem("Total HPP Produksi", dto.ToComparison( + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudgetAmount), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealizationAmount), + )), + } + + return items +} + +func (s closingService) calculateHppSummary(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) dto.SummaryHpp { + var totalBudget float64 + var totalRealization float64 + + for _, budget := range budgets { + totalBudget += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealization += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudget / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudget / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealization / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealization / totalWeightSold + } + + return dto.ToSummaryHpp("Total HPP", dto.ToComparison( + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), + )) +} + +func (s closingService) buildPenjualanItems(deliveryProducts []entity.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []dto.PLItem { + var totalAmount float64 + + for _, delivery := range deliveryProducts { + totalAmount += delivery.TotalPrice + } + + rpPerBird := 0.0 + rpPerKg := 0.0 + if totalPopulation > 0 { + rpPerBird = totalAmount / totalPopulation + } + if totalWeightSold > 0 { + rpPerKg = totalAmount / totalWeightSold + } + + items := []dto.PLItem{ + dto.ToPLItem("Penjualan", dto.ToFinancialMetrics(rpPerBird, rpPerKg, totalAmount)), + } + + return items +} + +func (s closingService) buildPembelianItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalPopulation, totalWeightSold float64) []dto.PLItem { + var totalBudget float64 + var totalRealization float64 + + for _, budget := range budgets { + totalBudget += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealization += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudget / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudget / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealization / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealization / totalWeightSold + } + + items := []dto.PLItem{ + dto.ToPLItem("Beban Pokok Produksi", dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget)), + dto.ToPLItem("Realisasi Beban Pokok", dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization)), + } + + return items +} + +func (s closingService) calculatePLSummary(penjualanItems, pembelianItems []dto.PLItem) dto.PLSummaryGroup { + var totalPenjualan float64 + var totalPenjualanPerBird float64 + var totalPembelian float64 + var totalPembelianPerBird float64 + + for _, item := range penjualanItems { + totalPenjualan += item.Amount + totalPenjualanPerBird += item.RpPerBird + } + + for _, item := range pembelianItems { + totalPembelian += item.Amount + totalPembelianPerBird += item.RpPerBird + } + + grossProfit := totalPenjualan - totalPembelian + grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird + + return dto.ToPLSummaryGroup( + dto.ToPLSummaryItem("Laba Kotor", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + dto.ToPLSummaryItem("Sub Total", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + dto.ToPLSummaryItem("Laba Bersih", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + ) +} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index e4d57b79..d1931cdd 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -46,10 +46,10 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock.Nonstock.Uom"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Where("expenses.category = ?", "BOP"). + Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). + Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). + Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID). Find(&realizations).Error return realizations, err } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 85d850a6..94d23103 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -31,8 +32,6 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct - // JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas - // Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter db := r.DB().WithContext(ctx). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). @@ -91,16 +90,17 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Warehouse"). + Preload("ProductWarehouse.ProjectFlockKandang") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } - if filters.ProductId > 0 { + if filters.ProductId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") } @@ -109,8 +109,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.Search != "" { - db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", - "%"+filters.Search+"%") + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + + if filters.Search != "" { + searchPattern := "%" + filters.Search + "%" + db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) } if filters.CustomerId > 0 { @@ -121,10 +126,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) } - if filters.MarketingId > 0 { - db = db.Where("marketings.id = ?", filters.MarketingId) - } - if filters.ProductId > 0 { db = db.Where("product_warehouses.product_id = ?", filters.ProductId) } @@ -133,17 +134,90 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.ProjectFlockKandangId > 0 { - db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) - } - - if filters.DeliveryDate != "" { - if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { - nextDate := deliveryDate.AddDate(0, 0, 1) - db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) + if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { + if filters.FilterBy == "delivery_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.created_at >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.created_at < ?", nextDate) + } + } } } + sortColumn := "marketing_delivery_products.id" + sortOrder := "DESC" + + if filters.SortBy != "" { + switch filters.SortBy { + case "delivery_date": + sortColumn = "marketing_delivery_products.delivery_date" + case "customer": + sortColumn = "customers.name" + if !containsJoin(db, "customers") { + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + case "warehouse": + sortColumn = "warehouses.name" + if !containsJoin(db, "warehouses") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") + } + case "product": + sortColumn = "products.name" + if !containsJoin(db, "products") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") + } + case "sales_person": + sortColumn = "sales_users.name" + if !containsJoin(db, "sales_users") { + db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") + } + case "vehicle_number": + sortColumn = "marketing_delivery_products.vehicle_number" + case "sales_amount": + sortColumn = "marketing_delivery_products.total_price" + case "hpp_amount": + sortColumn = "marketing_delivery_products.total_price" + case "qty": + sortColumn = "marketing_delivery_products.qty" + case "average_weight": + sortColumn = "marketing_delivery_products.avg_weight" + case "total_weight": + sortColumn = "marketing_delivery_products.total_weight" + case "sales_price": + sortColumn = "marketing_delivery_products.unit_price" + case "hpp_price": + sortColumn = "marketing_delivery_products.unit_price" + case "aging_days": + sortColumn = "marketing_delivery_products.delivery_date" + } + } + + if filters.SortOrder != "" && (filters.SortOrder == "asc" || filters.SortOrder == "desc") { + sortOrder = strings.ToUpper(filters.SortOrder) + } + + db = db.Order(sortColumn + " " + sortOrder) + if err := db.Count(&total).Error; err != nil { return nil, 0, err } @@ -151,10 +225,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if err := db. Offset(offset). Limit(limit). - Order("marketing_delivery_products.id DESC"). Find(&deliveryProducts).Error; err != nil { return nil, 0, err } return deliveryProducts, total, nil } + +func containsJoin(db *gorm.DB, tableName string) bool { + statement := db.Statement + joinSQL := statement.SQL.String() + return strings.Contains(joinSQL, "JOIN "+tableName) +} diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..b94ec8c2 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -62,16 +62,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { query := &validation.MarketingQuery{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - Search: ctx.Query("search", ""), - CustomerId: int64(ctx.QueryInt("customer_id", 0)), - ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), - DeliveryDate: ctx.Query("delivery_date", ""), - ProductId: int64(ctx.QueryInt("product_id", 0)), - WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), - SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), - MarketingId: int64(ctx.QueryInt("marketing_id", 0)), + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + Search: ctx.Query("search", ""), + CustomerId: int64(ctx.QueryInt("customer_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), + SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), + FilterBy: ctx.Query("filter_by", ""), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + SortBy: ctx.Query("sort_by", ""), + SortOrder: ctx.Query("sort_order", ""), } if query.Page < 1 || query.Limit < 1 { @@ -84,7 +86,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { } return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ + JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 9cbd57ba..77c5f5d8 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,219 +1,121 @@ package dto import ( - "time" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +// === Main Report Item DTO === -type RepportMarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - Notes string `json:"notes"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type RepportMarketingItemDTO struct { + DoDate string `json:"do_date"` + RealizationDate string `json:"realization_date"` + AgingDays int `json:"aging_days"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + DoNumber string `json:"do_number"` + Sales *userDTO.UserRelationDTO `json:"sales,omitempty"` + VehicleNumber string `json:"vehicle_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + MarketingType string `json:"marketing_type"` + Qty float64 `json:"qty"` + AverageWeightKg float64 `json:"average_weight_kg"` + TotalWeightKg float64 `json:"total_weight_kg"` + SalesPricePerKg float64 `json:"sales_price_per_kg"` + HppPricePerKg float64 `json:"hpp_price_per_kg"` + SalesAmount float64 `json:"sales_amount"` + HppAmount float64 `json:"hpp_amount"` } -type RepportMarketingProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} +// === Report Response DTO === -type RepportMarketingDeliveryDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - VehicleNumber string `json:"vehicle_number"` - DoNumber string `json:"do_number"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -type RepportMarketingListDTO struct { - RepportMarketingBaseDTO - MarketingProduct RepportMarketingProductDTO `json:"marketing_product"` - MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"` - TotalMarketingProduct float64 `json:"total_marketing_product"` - TotalMarketingDelivery float64 `json:"total_marketing_delivery"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` +type RepportMarketingResponseDTO struct { + Items []RepportMarketingItemDTO `json:"items"` } // === MAPPERS === -func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO { - if m == nil { - return RepportMarketingBaseDTO{} +// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { + agingDays := 0 + + doDate := "" + if mdp.DeliveryDate != nil { + doDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var customer *customerDTO.CustomerRelationDTO - if m.Customer.Id != 0 { - mapped := customerDTO.ToCustomerRelationDTO(m.Customer) - customer = &mapped + realizationDate := "" + if mdp.DeliveryDate != nil { + realizationDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var salesPerson *userDTO.UserRelationDTO - if m.SalesPerson.Id != 0 { - mapped := userDTO.ToUserRelationDTO(m.SalesPerson) - salesPerson = &mapped + // Calculate sales_amount = total_weight_kg * sales_price_per_kg + salesAmount := mdp.TotalWeight * mdp.UnitPrice + // Calculate hpp_amount = total_weight_kg * hpp_price_per_kg + hppAmount := mdp.TotalWeight * hppPricePerKg + + item := RepportMarketingItemDTO{ + DoDate: doDate, + RealizationDate: realizationDate, + AgingDays: agingDays, + DoNumber: mdp.MarketingProduct.Marketing.SoNumber, + MarketingType: "ayam", + Qty: mdp.Qty, + AverageWeightKg: mdp.AvgWeight, + TotalWeightKg: mdp.TotalWeight, + SalesPricePerKg: mdp.UnitPrice, + HppPricePerKg: hppPricePerKg, + SalesAmount: salesAmount, + HppAmount: hppAmount, } - return RepportMarketingBaseDTO{ - Id: m.Id, - SoNumber: m.SoNumber, - SoDate: m.SoDate, - Customer: customer, - SalesPerson: salesPerson, - Notes: m.Notes, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, - } -} - -func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO { - if mp == nil { - return RepportMarketingProductDTO{} + // Map warehouse with full details + if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { + mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) + item.Warehouse = &mapped } - var product *productDTO.ProductRelationDTO - if mp.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) - product = &mapped + // Map customer using CustomerRelationDTO + if mdp.MarketingProduct.Marketing.CustomerId != 0 { + mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) + item.Customer = &mapped } - return RepportMarketingProductDTO{ - Id: mp.Id, - MarketingProductId: mp.Id, - Qty: mp.Qty, - UnitPrice: mp.UnitPrice, - AvgWeight: mp.AvgWeight, - TotalWeight: mp.TotalWeight, - TotalPrice: mp.TotalPrice, - Product: product, - CreatedAt: time.Now(), - } -} - -func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO { - if mdp == nil { - return RepportMarketingDeliveryDTO{} + // Map sales person + if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { + mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) + item.Sales = &mapped } - var product *productDTO.ProductRelationDTO - if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { + // Map vehicle number + item.VehicleNumber = mdp.VehicleNumber + + // Map product using ProductRelationDTO + if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) - product = &mapped + item.Product = &mapped } - warehouseId := uint(0) - if mdp.MarketingProduct.ProductWarehouse.Id != 0 { - warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId - } - - doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId) - - return RepportMarketingDeliveryDTO{ - Id: mdp.Id, - MarketingProductId: mdp.MarketingProductId, - Qty: mdp.Qty, - UnitPrice: mdp.UnitPrice, - TotalWeight: mdp.TotalWeight, - AvgWeight: mdp.AvgWeight, - TotalPrice: mdp.TotalPrice, - DeliveryDate: mdp.DeliveryDate, - VehicleNumber: mdp.VehicleNumber, - DoNumber: doNumber, - Product: product, - CreatedAt: time.Now(), - } + return item } -func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO { - var marketingProduct RepportMarketingProductDTO - var marketingDelivery RepportMarketingDeliveryDTO - - if mp != nil { - marketingProduct = ToRepportMarketingProductDTO(mp) - } - - if mdp != nil { - marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber) - } - - totalMarketingProduct := float64(0) - totalMarketingDelivery := float64(0) - - if mp != nil { - totalMarketingProduct = mp.Qty * mp.UnitPrice - } - - if mdp != nil { - totalMarketingDelivery = mdp.Qty * mdp.UnitPrice - } - - return RepportMarketingListDTO{ - RepportMarketingBaseDTO: baseDTO, - MarketingProduct: marketingProduct, - MarketingDelivery: marketingDelivery, - TotalMarketingProduct: totalMarketingProduct, - TotalMarketingDelivery: totalMarketingDelivery, - LatestApproval: latestApproval, +// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg)) } + return items } -func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { - result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) +// ToRepportMarketingResponseDTO creates complete marketing report response +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) - marketingMap := make(map[uint]entity.MarketingDeliveryProduct) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.Marketing.Id == 0 { - continue - } - marketingID := dp.MarketingProduct.Marketing.Id - if _, exists := marketingMap[marketingID]; !exists { - marketingMap[marketingID] = dp - } + return RepportMarketingResponseDTO{ + Items: items, } - - for _, deliveryProduct := range marketingMap { - if deliveryProduct.MarketingProduct.Marketing.Id == 0 { - continue - } - - marketing := &deliveryProduct.MarketingProduct.Marketing - baseDTO := ToRepportMarketingBaseDTO(marketing) - - var latestApproval *approvalDTO.ApprovalRelationDTO - if marketing.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped - } - - mdp := &deliveryProduct - dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval) - result = append(result, dto) - } - - return result } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..4db200ab 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1,6 +1,9 @@ package service import ( + "context" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -18,7 +21,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) - GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) } type repportService struct { @@ -77,7 +80,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer return result, total, nil } -func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { +func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -89,27 +92,88 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - marketingIDMap := make(map[uint]bool) - marketingIDs := make([]uint, 0) - for _, dp := range deliveryProducts { - if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] { - marketingIDs = append(marketingIDs, marketingID) - marketingIDMap[marketingID] = true - } - } + projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) + hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) + items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) - approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("LatestByTargets error: %v", err) - } - - for i := range deliveryProducts { - if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil { - deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval - } - } - - return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil + return items, total, nil +} + +func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint { + projectFlockIDMap := make(map[uint]bool) + projectFlockIDs := make([]uint, 0) + + for _, dp := range deliveryProducts { + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] { + projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId) + projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true + } + } + } + + return projectFlockIDs +} + +func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { + hppMap := make(map[uint]float64) + for _, projectFlockID := range projectFlockIDs { + hppPerKg := s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + hppMap[projectFlockID] = hppPerKg + } + return hppMap +} + +func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO { + items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts)) + for _, dp := range deliveryProducts { + hppPerKg := float64(0) + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + } + items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg)) + } + return items +} + +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { + if projectFlockID == 0 { + return 0 + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0 + } + + if len(realizations) == 0 { + return 0 + } + + totalActualCost := float64(0) + for _, realization := range realizations { + cost := realization.Price * realization.Qty + totalActualCost += cost + } + + if totalActualCost == 0 { + return 0 + } + + totalWeightSold := float64(0) + for _, dp := range deliveryProducts { + if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && + dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID { + totalWeightSold += dp.TotalWeight + } + } + + if totalWeightSold == 0 { + return 0 + } + + hppPerKg := totalActualCost / totalWeightSold + return hppPerKg } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..e568952d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -16,14 +16,16 @@ type ExpenseQuery struct { } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - DeliveryDate string `query:"delivery_date" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - MarketingId int64 `query:"marketing_id" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` } From 40f192660d4d1b361153458baf973ba0084a189d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 17 Dec 2025 11:30:49 +0700 Subject: [PATCH 06/31] Feat[BE]:: adjust marketing report API --- .../salesorder_delivery_product.repository.go | 30 +++--- .../controllers/repport.controller.go | 39 +++++++- .../repports/dto/repportMarketing.dto.go | 92 +++++++++++++------ .../repports/services/repport.service.go | 14 ++- .../validations/repport.validation.go | 4 +- 5 files changed, 131 insertions(+), 48 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 94d23103..8d895e34 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -135,7 +135,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { - if filters.FilterBy == "delivery_date" { + if filters.FilterBy == "so_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.so_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.so_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { if filters.StartDate != "" { if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) @@ -147,18 +159,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) } } - } else if filters.FilterBy == "realization_date" { - if filters.StartDate != "" { - if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { - db = db.Where("marketings.created_at >= ?", startDate) - } - } - if filters.EndDate != "" { - if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { - nextDate := endDate.AddDate(0, 0, 1) - db = db.Where("marketings.created_at < ?", nextDate) - } - } } } @@ -167,7 +167,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if filters.SortBy != "" { switch filters.SortBy { - case "delivery_date": + case "so_date": + sortColumn = "marketings.so_date" + case "realization_date": sortColumn = "marketing_delivery_products.delivery_date" case "customer": sortColumn = "customers.name" diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index b94ec8c2..d00a3ff5 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -11,6 +11,17 @@ import ( "github.com/gofiber/fiber/v2" ) +// === Marketing Report Response === + +type MarketingReportResponse struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta response.Meta `json:"meta"` + Data []dto.RepportMarketingItemDTO `json:"data"` + Total *dto.Summary `json:"total,omitempty"` +} + type RepportController struct { RepportService service.RepportService } @@ -85,8 +96,31 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { return err } + // Calculate total summary from result items + var total *dto.Summary + if len(result) > 0 { + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, item := range result { + totalQty += int(item.Qty) + totalWeightKg += item.TotalWeightKg + totalSalesAmount += int64(item.SalesAmount) + totalHppAmount += int64(item.HppAmount) + } + + total = &dto.Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + } + } + return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{ + JSON(MarketingReportResponse{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", @@ -96,6 +130,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: result, + Data: result, + Total: total, }) } diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 77c5f5d8..98ec9888 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,6 +1,9 @@ package dto import ( + "fmt" + "time" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" @@ -8,11 +11,10 @@ import ( userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === Main Report Item DTO === - type RepportMarketingItemDTO struct { - DoDate string `json:"do_date"` - RealizationDate string `json:"realization_date"` + ID int `json:"id"` + SoDate time.Time `json:"so_date"` + RealizationDate time.Time `json:"realization_date"` AgingDays int `json:"aging_days"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` @@ -30,70 +32,70 @@ type RepportMarketingItemDTO struct { HppAmount float64 `json:"hpp_amount"` } -// === Report Response DTO === +type Summary struct { + TotalQty int `json:"total_qty"` + TotalWeightKg float64 `json:"total_weight_kg"` + TotalSalesAmount int64 `json:"total_sales_amount"` + TotalHppAmount int64 `json:"total_hpp_amount"` +} type RepportMarketingResponseDTO struct { Items []RepportMarketingItemDTO `json:"items"` + Total *Summary `json:"total,omitempty"` } -// === MAPPERS === - -// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { + soDate := time.Time{} agingDays := 0 - - doDate := "" - if mdp.DeliveryDate != nil { - doDate = mdp.DeliveryDate.Format("02-Jan-2006") + if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { + soDate = mdp.MarketingProduct.Marketing.SoDate + agingDays = int(time.Now().Sub(soDate).Hours() / 24) } - realizationDate := "" + realizationDate := time.Time{} if mdp.DeliveryDate != nil { - realizationDate = mdp.DeliveryDate.Format("02-Jan-2006") + realizationDate = *mdp.DeliveryDate } - // Calculate sales_amount = total_weight_kg * sales_price_per_kg - salesAmount := mdp.TotalWeight * mdp.UnitPrice - // Calculate hpp_amount = total_weight_kg * hpp_price_per_kg - hppAmount := mdp.TotalWeight * hppPricePerKg + doNumber := generateDoNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) + + totalWeightKg := mdp.Qty * mdp.AvgWeight + salesAmount := totalWeightKg * mdp.UnitPrice + hppAmount := totalWeightKg * hppPricePerKg item := RepportMarketingItemDTO{ - DoDate: doDate, + ID: int(mdp.Id), + SoDate: soDate, RealizationDate: realizationDate, AgingDays: agingDays, - DoNumber: mdp.MarketingProduct.Marketing.SoNumber, + DoNumber: doNumber, MarketingType: "ayam", Qty: mdp.Qty, AverageWeightKg: mdp.AvgWeight, - TotalWeightKg: mdp.TotalWeight, + TotalWeightKg: totalWeightKg, SalesPricePerKg: mdp.UnitPrice, HppPricePerKg: hppPricePerKg, SalesAmount: salesAmount, HppAmount: hppAmount, } - // Map warehouse with full details if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) item.Warehouse = &mapped } - // Map customer using CustomerRelationDTO if mdp.MarketingProduct.Marketing.CustomerId != 0 { mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) item.Customer = &mapped } - // Map sales person if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) item.Sales = &mapped } - // Map vehicle number item.VehicleNumber = mdp.VehicleNumber - // Map product using ProductRelationDTO if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) item.Product = &mapped @@ -102,7 +104,6 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK return item } -// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { @@ -111,11 +112,46 @@ func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPrice return items } -// ToRepportMarketingResponseDTO creates complete marketing report response +func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { + if len(mdps) == 0 { + return nil + } + + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, mdp := range mdps { + calculatedTotalWeight := mdp.Qty * mdp.AvgWeight + totalQty += int(mdp.Qty) + totalWeightKg += calculatedTotalWeight + totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) + totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + } + + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + } +} + +func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string { + dateStr := "" + if deliveryDate != nil { + dateStr = deliveryDate.Format("20060102") + } + return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) +} + func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) + total := ToSummary(mdps, hppPricePerKg) return RepportMarketingResponseDTO{ Items: items, + Total: total, } } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 4db200ab..553fc7af 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -152,12 +152,22 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc return 0 } - totalActualCost := float64(0) + costBop := float64(0) + for _, realization := range realizations { cost := realization.Price * realization.Qty - totalActualCost += cost + category := "" + if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil { + category = realization.ExpenseNonstock.Expense.Category + } + + if category == "BOP" { + costBop += cost + } } + totalActualCost := costBop + if totalActualCost == 0 { return 0 } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index e568952d..6b3bd71e 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -23,9 +23,9 @@ type MarketingQuery struct { ProductId int64 `query:"product_id" validate:"omitempty"` WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` } From d9a1372077039d14f03d7ab5f0a00a0889568710 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 17 Dec 2025 11:34:08 +0700 Subject: [PATCH 07/31] feat[BE]:: add totalHppPricePerKg to marketing report summary --- .../repports/dto/repportMarketing.dto.go | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 98ec9888..dc4baabd 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -33,10 +33,11 @@ type RepportMarketingItemDTO struct { } type Summary struct { - TotalQty int `json:"total_qty"` - TotalWeightKg float64 `json:"total_weight_kg"` - TotalSalesAmount int64 `json:"total_sales_amount"` - TotalHppAmount int64 `json:"total_hpp_amount"` + TotalQty int `json:"total_qty"` + TotalWeightKg float64 `json:"total_weight_kg"` + TotalSalesAmount int64 `json:"total_sales_amount"` + TotalHppAmount int64 `json:"total_hpp_amount"` + TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"` } type RepportMarketingResponseDTO struct { @@ -130,11 +131,17 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) } + totalHppPricePerKg := float64(0) + if totalWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + } + return &Summary{ - TotalQty: totalQty, - TotalWeightKg: totalWeightKg, - TotalSalesAmount: totalSalesAmount, - TotalHppAmount: totalHppAmount, + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, } } From 21d22c20a3addb5d654b4f9ec7fd5bdfc8730eef Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Wed, 17 Dec 2025 13:20:00 +0700 Subject: [PATCH 08/31] add constant flag --- internal/utils/constant.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 6594ac6b..8b51619b 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -29,6 +29,18 @@ const ( FlagVitamin FlagType = "VITAMIN" FlagKimia FlagType = "KIMIA" FlagEkspedisi FlagType = "EKSPEDISI" + + // flag ayam + FlagAyamAfkir FlagType = "AYAM-AFKIR" + FlagAyamCulling FlagType = "AYAM-CULLING" + FlagAyamMati FlagType = "AYAM-MATI" + + //flag telur + FlagTelur FlagType = "TELUR" + FlagTelurUtuh FlagType = "TELUR-UTUH" + FlagTelurPecah FlagType = "TELUR-PECAH" + FlagTelurPutih FlagType = "TELUR-PUTIH" + FlagTelurRetak FlagType = "TELUR-RETAK" ) const ( @@ -205,8 +217,8 @@ const ( ) var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ - RecordingStepPengajuan: "Pengajuan", - RecordingStepDisetujui: "Disetujui", + RecordingStepPengajuan: "Pengajuan", + RecordingStepDisetujui: "Disetujui", } // ------------------------------------------------------------------- From 3bfc401206eed1e05a6fff56057dbd7c40966856 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 17 Dec 2025 13:56:51 +0700 Subject: [PATCH 09/31] Feat(BE-334): make reporting closing hpp for project_flock_kandang --- .../controllers/closing.controller.go | 32 ++++++++++++++++++- internal/modules/closings/route.go | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 2b29429e..113aa667 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -251,7 +251,7 @@ func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { projectFlockID, err := strconv.Atoi(param) if err != nil || projectFlockID <= 0 { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") } var projectFlockKandangID *uint @@ -277,3 +277,33 @@ func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { Data: result, }) } + +func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error { + projectParam := c.Params("project_flock_id") + kandangParam := c.Params("project_flock_kandang_id") + + projectFlockID, err := strconv.Atoi(projectParam) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pfkID, err := strconv.Atoi(kandangParam) + if err != nil || pfkID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + + kandangID := uint(pfkID) + + result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expedition HPP successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 526ab3b9..8a155bd0 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -28,4 +28,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) route.Get("/:project_flock_id/expedition-hpp", ctrl.GetExpeditionHPP) + route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", ctrl.GetExpeditionHPPByKandang) } From 1b238616568f2cba52eba12eb0a49758c5b0522d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 09:58:31 +0700 Subject: [PATCH 10/31] feat[BE]: membetulkan perhitungan hpp di module penjualan harian --- .../closings/dto/closingKeuangan.dto.go | 434 ++++++++++++++++-- internal/modules/closings/module.go | 4 +- .../closings/services/closing.service.go | 221 +-------- .../expense_realization.repository.go | 4 +- .../salesorder_delivery_product.repository.go | 3 +- .../repositories/recording.repository.go | 71 +++ .../repositories/purchase.repository.go | 12 + .../controllers/repport.controller.go | 24 +- .../repports/dto/repportMarketing.dto.go | 47 +- internal/modules/repports/module.go | 6 +- .../repports/services/repport.service.go | 65 ++- 11 files changed, 592 insertions(+), 299 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index d380dc3d..cf4b5b54 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,5 +1,12 @@ package dto +import ( + "strings" + + "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + // === BASE METRICS === type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` @@ -28,9 +35,7 @@ type SummaryHpp struct { Comparison } -// Ini adalah struct mandiri untuk bagian HPP Purchases type HppPurchasesSection struct { - Title string `json:"title"` Hpp []HppGroup `json:"hpp"` SummaryHpp SummaryHpp `json:"summary_hpp"` } @@ -58,14 +63,11 @@ type ProfitLossData struct { Summary PLSummaryGroup `json:"summary"` } -// Ini adalah struct mandiri untuk bagian Profit Loss type ProfitLossSection struct { - Title string `json:"title"` - Data ProfitLossData `json:"data"` + Data ProfitLossData `json:"data"` } // === RESPONSE DTO (ROOT) === -// Sekarang Root-nya terlihat sangat bersih dan tidak "janggal" lagi type ReportResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"` ProfitLoss ProfitLossSection `json:"profit_loss"` @@ -73,7 +75,6 @@ type ReportResponse struct { // === MAPPER FUNCTIONS === -// FinancialMetrics Mappers func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { return FinancialMetrics{ RpPerBird: rpPerBird, @@ -82,7 +83,6 @@ func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { } } -// Comparison Mappers func ToComparison(budgeting, realization FinancialMetrics) Comparison { return Comparison{ Budgeting: budgeting, @@ -90,40 +90,141 @@ func ToComparison(budgeting, realization FinancialMetrics) Comparison { } } -// HppItem Mappers -func ToHppItem(itemType string, comparison Comparison) HppItem { - return HppItem{ - Type: itemType, - Comparison: comparison, - } +// === HPP PENGELUARAN (from Purchase Items) === + +func getFlagLabel(flagType utils.FlagType) string { + return "Pembelian " + string(flagType) } -// HppGroup Mappers -func ToHppGroup(groupName string, items []HppItem) HppGroup { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem { + flags := []utils.FlagType{ + utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, + utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, + utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia, + } + + items := []HppItem{} + seenFlags := make(map[utils.FlagType]bool) + + for _, item := range purchaseItems { + if item.Product == nil || len(item.Product.Flags) == 0 { + continue + } + + for _, flag := range item.Product.Flags { + flagType := utils.FlagType(flag.Name) + + // Check if valid flag and not processed + isValid := false + for _, validFlag := range flags { + if validFlag == flagType { + isValid = true + break + } + } + + if isValid && !seenFlags[flagType] { + amount := sumPurchasesByFlag(purchaseItems, flagType) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + + items = append(items, HppItem{ + Type: getFlagLabel(flagType), + Comparison: ToComparison( + ToFinancialMetrics(rpPerBird, rpPerKg, amount), + ToFinancialMetrics(rpPerBird, rpPerKg, amount), // Same for purchase + ), + }) + seenFlags[flagType] = true + } + } + } + + return items +} + +// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === + +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup { + items := []HppItem{} + + // Overhead: all budgets vs (all expenses EXCEPT ekspedisi) + budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightSold) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + + if budgetAmount > 0 || realizationAmount > 0 { + items = append(items, HppItem{ + Type: "Pengeluaran Overhead", + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ), + }) + } + + // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI + ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold) + + if ekspedisiAmount > 0 { + items = append(items, HppItem{ + Type: "Beban Ekspedisi", + Comparison: ToComparison( + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ), + }) + } + return HppGroup{ - GroupName: groupName, + GroupName: "HPP dan Bahan Baku", Data: items, } } -// SummaryHpp Mappers -func ToSummaryHpp(label string, comparison Comparison) SummaryHpp { +// === HPP SUMMARY === + +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp { + // Budget: purchases + budgets + purchaseTotal := sumPurchaseTotal(purchaseItems) + budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + totalBudget := purchaseTotal + budgetTotal + + // Realization: all expenses + totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) + + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold) + return SummaryHpp{ - Label: label, - Comparison: comparison, + Label: label, + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), + ), } } -// HppPurchasesSection Mappers -func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection { + hppGroups := []HppGroup{ + { + GroupName: "HPP dan Pengeluaran", + Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation), + }, + ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation), + } + + summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + return HppPurchasesSection{ - Title: title, Hpp: hppGroups, SummaryHpp: summaryHpp, } } -// PLItem Mappers +// === PROFIT & LOSS === + func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { return PLItem{ Type: itemType, @@ -131,7 +232,6 @@ func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { } } -// PLSummaryItem Mappers func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { return PLSummaryItem{ Label: label, @@ -139,33 +239,106 @@ func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { } } -// PLSummaryGroup Mappers -func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { - return PLSummaryGroup{ - GrossProfit: grossProfit, - SubTotal: subTotal, - NetProfit: netProfit, +func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { + for _, item := range items { + totalAmount += item.Amount + totalPerBird += item.RpPerBird + } + return +} + +func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { + // Categorize deliveries by sales type based on Product flags + categorized := categorizeDeliveriesBySalesType(deliveryProducts) + + items := []PLItem{} + + // Process each sales category + for salesType, deliveries := range categorized { + amount := sumDeliveriesByCategory(deliveries) + + // Use totalPopulation and totalWeightSold for per-unit calculations + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + + items = append(items, ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))) + } + + return items +} + +func ToPembelianItems(purchases []entities.PurchaseItem, totalPopulation, totalWeightSold float64) []PLItem { + amount := sumPurchasesByFilter(purchases, func(item *entities.PurchaseItem) bool { + if item.Product == nil || len(item.Product.Flags) == 0 { + return false + } + for _, flag := range item.Product.Flags { + flagType := strings.ToUpper(flag.Name) + if flagType == string(utils.FlagDOC) || flagType == string(utils.FlagOVK) || flagType == string(utils.FlagPakan) { + return true + } + } + return false + }) + + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } } -// ProfitLossData Mappers -func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { +func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { + realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), + } +} + +func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { + amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + return []PLItem{ + ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + } +} + +func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { + totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) + totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) + totalOverhead, _ := sumPLItems(overheadItems) + totalEkspedisi, _ := sumPLItems(ekspedisiItems) + + grossProfit := totalPenjualan - totalPembelian + grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird + + totalOtherExpenses := totalOverhead + totalEkspedisi + + netProfit := grossProfit - totalOtherExpenses + netProfitPerBird := grossProfitPerBird - 0.0 + + return PLSummaryGroup{ + GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(0, 0, totalOtherExpenses)), + NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), + } +} + +func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { + summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + return ProfitLossData{ - Penjualan: penjualan, - Pembelian: pembelian, + Penjualan: penjualanItems, + Pembelian: pembelianItems, Summary: summary, } } -// ProfitLossSection Mappers -func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection { +func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection { return ProfitLossSection{ - Title: title, - Data: data, + Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems), } } -// ReportResponse Mappers func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { return ReportResponse{ HppPurchases: hppPurchases, @@ -173,14 +346,175 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -// Helper function to create a complete financial report -func BuildFinancialReport( - hppGroups []HppGroup, - summaryHpp SummaryHpp, - penjualan, pembelian []PLItem, - plSummary PLSummaryGroup, -) ReportResponse { - hppSection := ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) - plSection := ToProfitLossSection("Laporan Laba Rugi", ToProfitLossData(penjualan, pembelian, plSummary)) +// === MAIN BUILDER === + +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse { + var totalPopulation float64 + var totalWeightSold float64 + + for _, chickin := range chickins { + totalPopulation += chickin.UsageQty + } + + for _, delivery := range deliveryProducts { + totalWeightSold += delivery.TotalWeight + } + + hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + + penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) + pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold) + overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold) + ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold) + 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 filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { + return func(item *entities.PurchaseItem) bool { + if item.Product == nil || len(item.Product.Flags) == 0 { + return false + } + for _, flag := range item.Product.Flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false + } +} + +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 + } + nonstock := realization.ExpenseNonstock.Nonstock + for _, flag := range nonstock.Flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false + } +} + +func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { + hasFlag := filterRealizationByNonstockFlag(flagType) + return func(realization *entities.ExpenseRealization) bool { + return !hasFlag(realization) + } +} + +func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { + amount := 0.0 + for i := range purchases { + if filter(&purchases[i]) { + amount += purchases[i].TotalPrice + } + } + return amount +} + +func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 { + return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType)) +} + +func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 { + amount := 0.0 + for i := range purchases { + amount += purchases[i].TotalPrice + } + return amount +} + +func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 { + amount := 0.0 + for i := range budgets { + if filter(&budgets[i]) { + amount += budgets[i].Price * budgets[i].Qty + } + } + return amount +} + +func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 { + amount := 0.0 + for i := range realizations { + if filter(&realizations[i]) { + amount += realizations[i].Price * realizations[i].Qty + } + } + return amount +} + +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 "Penjualan Ayam Besar" + } + + for _, flag := range product.Flags { + flagType := utils.FlagType(strings.ToUpper(flag.Name)) + + if isEggProductFlag(flagType) { + return "Penjualan Telur" + } + if isChickenProductFlag(flagType) { + return "Penjualan Ayam Besar" + } + } + + return "Penjualan Ayam Besar" +} + +func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { + categorized := make(map[string][]entities.MarketingDeliveryProduct) + + for _, delivery := range deliveries { + product := delivery.MarketingProduct.ProductWarehouse.Product + salesType := getSalesTypeFromProductFlags(&product) + + categorized[salesType] = append(categorized[salesType], delivery) + } + + return categorized +} + +func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 { + amount := 0.0 + for _, delivery := range deliveries { + amount += delivery.TotalPrice + } + return amount +} diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index c3de4a86..494f2736 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -13,6 +13,7 @@ import ( rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -30,10 +31,11 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) + purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index e6e74d45..29001149 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -15,6 +15,7 @@ import ( marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -45,9 +46,10 @@ type closingService struct { ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository + PurchaseRepo purchaseRepository.PurchaseRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -59,6 +61,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, + PurchaseRepo: purchaseRepo, } } @@ -386,24 +389,35 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } - _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + 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 { - s.Log.Errorf("Failed to get project flock %d for closing keuangan: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) - if err != nil { - s.Log.Errorf("Failed to get budgets for project flock %d: %+v", projectFlockID, err) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") } + purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(c.Context(), projectFlockID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch purchase items") + } + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { - s.Log.Errorf("Failed to get realizations for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") } @@ -413,204 +427,15 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* Preload("MarketingProduct.ProductWarehouse.Product") }) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - s.Log.Errorf("Failed to get delivery products for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") } chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { - s.Log.Errorf("Failed to get chickins for project flock %d: %+v", projectFlockID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } - var totalPopulation float64 - for _, chickin := range chickins { - totalPopulation += chickin.UsageQty - } - - var totalWeightSold float64 - for _, delivery := range deliveryProducts { - totalWeightSold += delivery.TotalWeight - } - - hppItems := s.buildHppItems(budgets, realizations, totalWeightSold, totalPopulation) - hppGroups := []dto.HppGroup{ - dto.ToHppGroup("Input Produksi", hppItems), - } - - summaryHpp := s.calculateHppSummary(budgets, realizations, totalWeightSold, totalPopulation) - - penjualanItems := s.buildPenjualanItems(deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := s.buildPembelianItems(budgets, realizations, totalPopulation, totalWeightSold) - plSummary := s.calculatePLSummary(penjualanItems, pembelianItems) - - hppSection := dto.ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) - plSection := dto.ToProfitLossSection("Laporan Laba Rugi", dto.ToProfitLossData(penjualanItems, pembelianItems, plSummary)) - - report := dto.ToReportResponse(hppSection, plSection) + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins) return &report, nil } - -func (s closingService) buildHppItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) []dto.HppItem { - var totalBudgetAmount float64 - var totalRealizationAmount float64 - - for _, budget := range budgets { - totalBudgetAmount += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealizationAmount += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudgetAmount / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudgetAmount / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealizationAmount / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealizationAmount / totalWeightSold - } - - items := []dto.HppItem{ - dto.ToHppItem("Total HPP Produksi", dto.ToComparison( - dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudgetAmount), - dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealizationAmount), - )), - } - - return items -} - -func (s closingService) calculateHppSummary(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) dto.SummaryHpp { - var totalBudget float64 - var totalRealization float64 - - for _, budget := range budgets { - totalBudget += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealization += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudget / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudget / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealization / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealization / totalWeightSold - } - - return dto.ToSummaryHpp("Total HPP", dto.ToComparison( - dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), - dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), - )) -} - -func (s closingService) buildPenjualanItems(deliveryProducts []entity.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []dto.PLItem { - var totalAmount float64 - - for _, delivery := range deliveryProducts { - totalAmount += delivery.TotalPrice - } - - rpPerBird := 0.0 - rpPerKg := 0.0 - if totalPopulation > 0 { - rpPerBird = totalAmount / totalPopulation - } - if totalWeightSold > 0 { - rpPerKg = totalAmount / totalWeightSold - } - - items := []dto.PLItem{ - dto.ToPLItem("Penjualan", dto.ToFinancialMetrics(rpPerBird, rpPerKg, totalAmount)), - } - - return items -} - -func (s closingService) buildPembelianItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalPopulation, totalWeightSold float64) []dto.PLItem { - var totalBudget float64 - var totalRealization float64 - - for _, budget := range budgets { - totalBudget += budget.Price * budget.Qty - } - - for _, realization := range realizations { - totalRealization += realization.Price * realization.Qty - } - - budgetRpPerBird := 0.0 - budgetRpPerKg := 0.0 - if totalPopulation > 0 { - budgetRpPerBird = totalBudget / totalPopulation - } - if totalWeightSold > 0 { - budgetRpPerKg = totalBudget / totalWeightSold - } - - realizationRpPerBird := 0.0 - realizationRpPerKg := 0.0 - if totalPopulation > 0 { - realizationRpPerBird = totalRealization / totalPopulation - } - if totalWeightSold > 0 { - realizationRpPerKg = totalRealization / totalWeightSold - } - - items := []dto.PLItem{ - dto.ToPLItem("Beban Pokok Produksi", dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget)), - dto.ToPLItem("Realisasi Beban Pokok", dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization)), - } - - return items -} - -func (s closingService) calculatePLSummary(penjualanItems, pembelianItems []dto.PLItem) dto.PLSummaryGroup { - var totalPenjualan float64 - var totalPenjualanPerBird float64 - var totalPembelian float64 - var totalPembelianPerBird float64 - - for _, item := range penjualanItems { - totalPenjualan += item.Amount - totalPenjualanPerBird += item.RpPerBird - } - - for _, item := range pembelianItems { - totalPembelian += item.Amount - totalPembelianPerBird += item.RpPerBird - } - - grossProfit := totalPenjualan - totalPembelian - grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird - - return dto.ToPLSummaryGroup( - dto.ToPLSummaryItem("Laba Kotor", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - dto.ToPLSummaryItem("Sub Total", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - dto.ToPLSummaryItem("Laba Bersih", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - ) -} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index d1931cdd..474b2962 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -44,6 +44,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock"). Preload("ExpenseNonstock.Nonstock"). Preload("ExpenseNonstock.Nonstock.Uom"). + Preload("ExpenseNonstock.Nonstock.Flags"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). @@ -66,7 +67,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context Preload("Expense.Supplier"). Preload("Kandang"). Preload("Kandang.Location"). - Preload("Nonstock") + Preload("Nonstock"). + Preload("Nonstock.Flags") }). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 8d895e34..b908681e 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -91,7 +91,8 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). Preload("ProductWarehouse.Warehouse"). - Preload("ProductWarehouse.ProjectFlockKandang") + Preload("ProductWarehouse.ProjectFlockKandang"). + Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 60457074..4a7e627c 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -45,6 +45,9 @@ type RecordingRepository interface { GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) + GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) + GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) + GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) } type RecordingRepositoryImpl struct { @@ -363,6 +366,74 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint return weight, true, nil } +func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { + if projectFlockID == 0 { + return 0, 0, nil + } + + // Get total chickin quantity for this ProjectFlock + totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Get total depletion for this ProjectFlock + totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Calculate actual quantity produced + actualQty := totalChickinQty - totalDepletion + + // Get latest average weight from RecordingBW + avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + // Calculate total weight + totalWeight = actualQty * avgWeight + + return totalWeight, actualQty, nil +} + +func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_depletions"). + Select("COALESCE(SUM(recording_depletions.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_bws"). + Select("COALESCE(AVG(recording_bws.avg_weight), 0)"). + Joins("JOIN recordings ON recordings.id = recording_bws.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings WHERE project_flock_kandangs_id = project_flock_kandangs.id)"). + Scan(&result).Error + return result, err +} + func nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 9f008b0d..2f9b2774 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -25,6 +25,7 @@ type PurchaseRepository interface { NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error + GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } type PurchaseRepositoryImpl struct { @@ -289,6 +290,17 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, return count > 0, nil } +func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + var items []entity.PurchaseItem + err := r.DB().WithContext(ctx). + Preload("Product"). + Preload("Product.Flags"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = purchase_items.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Find(&items).Error + return items, err +} + func parseNumericSuffix(value, prefix string) (int, bool) { if !strings.HasPrefix(value, prefix) { return 0, false diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index d00a3ff5..b5285c8e 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -96,28 +96,8 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { return err } - // Calculate total summary from result items - var total *dto.Summary - if len(result) > 0 { - totalQty := 0 - totalWeightKg := 0.0 - totalSalesAmount := int64(0) - totalHppAmount := int64(0) - - for _, item := range result { - totalQty += int(item.Qty) - totalWeightKg += item.TotalWeightKg - totalSalesAmount += int64(item.SalesAmount) - totalHppAmount += int64(item.HppAmount) - } - - total = &dto.Summary{ - TotalQty: totalQty, - TotalWeightKg: totalWeightKg, - TotalSalesAmount: totalSalesAmount, - TotalHppAmount: totalHppAmount, - } - } + + total := dto.ToSummaryFromDTOItems(result) return ctx.Status(fiber.StatusOK). JSON(MarketingReportResponse{ diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index dc4baabd..deadf3b8 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -50,7 +50,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK agingDays := 0 if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { soDate = mdp.MarketingProduct.Marketing.SoDate - agingDays = int(time.Now().Sub(soDate).Hours() / 24) + agingDays = int(time.Since(soDate).Hours() / 24) } realizationDate := time.Time{} @@ -113,6 +113,20 @@ func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPrice return items } +func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + hppPerKg := float64(0) + if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + } + items = append(items, ToRepportMarketingItemDTO(mdp, hppPerKg)) + } + return items +} + func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { if len(mdps) == 0 { return nil @@ -153,6 +167,37 @@ func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) } +func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { + if len(items) == 0 { + return nil + } + + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, item := range items { + totalQty += int(item.Qty) + totalWeightKg += item.TotalWeightKg + totalSalesAmount += int64(item.SalesAmount) + totalHppAmount += int64(item.HppAmount) + } + + totalHppPricePerKg := float64(0) + if totalWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + } + + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, + } +} + func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) total := ToSummary(mdps, hppPricePerKg) diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..f347ab69 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -11,6 +11,8 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" ) type RepportModule struct{} @@ -19,10 +21,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) + purchaseRepository := purchaseRepo.NewPurchaseRepository(db) + recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 553fc7af..5458a28d 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -12,6 +12,8 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -29,15 +31,19 @@ type repportService struct { Validate *validator.Validate ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository + PurchaseRepo purchaseRepo.PurchaseRepository + RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, + PurchaseRepo: purchaseRepo, + RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, } } @@ -94,7 +100,7 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) - items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) + items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) return items, total, nil } @@ -118,24 +124,33 @@ func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.Market func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { hppMap := make(map[uint]float64) for _, projectFlockID := range projectFlockIDs { - hppPerKg := s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + category := s.getProjectFlockCategory(deliveryProducts, projectFlockID) + hppPerKg := s.calculateHppByCategory(ctx, category, projectFlockID, deliveryProducts) hppMap[projectFlockID] = hppPerKg } return hppMap } -func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO { - items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts)) +func (s *repportService) calculateHppByCategory(ctx context.Context, category string, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { + switch utils.ProjectFlockCategory(category) { + case utils.ProjectFlockCategoryGrowing: + return s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + case utils.ProjectFlockCategoryLaying: + return 0 + default: + return 0 + } +} + +func (s *repportService) getProjectFlockCategory(deliveryProducts []entity.MarketingDeliveryProduct, projectFlockID uint) string { for _, dp := range deliveryProducts { - hppPerKg := float64(0) if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { - hppPerKg = hpp + if projectFlockKandang.ProjectFlockId == projectFlockID { + return projectFlockKandang.ProjectFlock.Category } } - items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg)) } - return items + return "" } func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { @@ -143,17 +158,22 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc return 0 } - realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { - return 0 + s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) } - if len(realizations) == 0 { - return 0 + costPurchase := float64(0) + for _, item := range purchaseItems { + costPurchase += item.TotalPrice + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("GetByProjectFlockID error: %v", err) } costBop := float64(0) - for _, realization := range realizations { cost := realization.Price * realization.Qty category := "" @@ -166,24 +186,21 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc } } - totalActualCost := costBop + totalActualCost := costPurchase + costBop if totalActualCost == 0 { return 0 } - totalWeightSold := float64(0) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && - dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID { - totalWeightSold += dp.TotalWeight - } + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - if totalWeightSold == 0 { + if totalWeightProduced == 0 { return 0 } - hppPerKg := totalActualCost / totalWeightSold + hppPerKg := totalActualCost / totalWeightProduced return hppPerKg } From 096a446450b5a3223642e73e7f5fb4e30a3d5668 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 10:45:04 +0700 Subject: [PATCH 11/31] feat[BE]: update HPP calculations to use totalWeightProduced and totalActualPopulation --- .../closings/dto/closingKeuangan.dto.go | 71 +++++++++---------- .../closings/dto/closingOverhead.dto.go | 13 ++-- internal/modules/closings/module.go | 4 +- .../closings/services/closing.service.go | 21 +++++- 4 files changed, 62 insertions(+), 47 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index cf4b5b54..13e7c196 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -96,7 +96,7 @@ func getFlagLabel(flagType utils.FlagType) string { return "Pembelian " + string(flagType) } -func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightSold, totalPopulation float64) []HppItem { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightProduced, totalPopulation float64) []HppItem { flags := []utils.FlagType{ utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, @@ -125,7 +125,7 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe if isValid && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) items = append(items, HppItem{ Type: getFlagLabel(flagType), @@ -144,14 +144,14 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === -func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppGroup { +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppGroup { items := []HppItem{} // Overhead: all budgets vs (all expenses EXCEPT ekspedisi) budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightSold) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) if budgetAmount > 0 || realizationAmount > 0 { items = append(items, HppItem{ @@ -165,7 +165,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightSold) + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced) if ekspedisiAmount > 0 { items = append(items, HppItem{ @@ -185,7 +185,7 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti // === HPP SUMMARY === -func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) SummaryHpp { +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) SummaryHpp { // Budget: purchases + budgets purchaseTotal := sumPurchaseTotal(purchaseItems) budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) @@ -194,8 +194,8 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ // Realization: all expenses totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) - budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightSold) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightSold) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightProduced) return SummaryHpp{ Label: label, @@ -206,16 +206,16 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ } } -func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightSold, totalPopulation float64) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection { hppGroups := []HppGroup{ { GroupName: "HPP dan Pengeluaran", - Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightSold, totalPopulation), + Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation), }, - ToHppBahanBakuGroup(budgets, realizations, totalWeightSold, totalPopulation), + ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation), } - summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) return HppPurchasesSection{ Hpp: hppGroups, @@ -266,37 +266,33 @@ func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.M return items } -func ToPembelianItems(purchases []entities.PurchaseItem, totalPopulation, totalWeightSold float64) []PLItem { - amount := sumPurchasesByFilter(purchases, func(item *entities.PurchaseItem) bool { - if item.Product == nil || len(item.Product.Flags) == 0 { - return false - } - for _, flag := range item.Product.Flags { - flagType := strings.ToUpper(flag.Name) - if flagType == string(utils.FlagDOC) || flagType == string(utils.FlagOVK) || flagType == string(utils.FlagPakan) { - return true - } - } - return false - }) +func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { + // Calculate total cost using same logic as report penjualan: + // Total Cost = All Purchase Items + All BOP Expenses + purchaseAmount := sumPurchaseTotal(purchases) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + // Get BOP expenses (all expenses except ekspedisi) + bopAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) + + totalCost := purchaseAmount + bopAmount + + rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Pembelian Sapronak Supplier", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + ToPLItem("Harga Pokok Penjualan (HPP)", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), } } -func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { +func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) return []PLItem{ ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), } } -func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightSold float64) []PLItem { +func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) return []PLItem{ ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), } @@ -348,7 +344,7 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec // === MAIN BUILDER === -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin) ReportResponse { +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced float64) ReportResponse { var totalPopulation float64 var totalWeightSold float64 @@ -360,12 +356,13 @@ func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entiti totalWeightSold += delivery.TotalWeight } - hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightSold, totalPopulation) + // Use totalWeightProduced for HPP calculation (not totalWeightSold) + hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, totalPopulation, totalWeightSold) - overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightSold) - ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightSold) + pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, totalPopulation, totalWeightProduced) + overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightProduced) + ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightProduced) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) diff --git a/internal/modules/closings/dto/closingOverhead.dto.go b/internal/modules/closings/dto/closingOverhead.dto.go index 95f3e10b..71975da1 100644 --- a/internal/modules/closings/dto/closingOverhead.dto.go +++ b/internal/modules/closings/dto/closingOverhead.dto.go @@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal return dto } -func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO { +func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { overheadsByNonstockID := make(map[uint]*OverheadDTO) latestDateByNonstockID := make(map[uint]string) @@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex for nonstockID, overhead := range overheadsByNonstockID { overhead.ActualDate = latestDateByNonstockID[nonstockID] - overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty) + + overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation) if overhead.ActualQuantity > 0 { overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity @@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex BudgetTotalAmount: totalBudgetAmount, ActualQuantity: totalActualQuantity, ActualTotalAmount: totalActualAmount, - CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty), + CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation), }, Overheads: overheadItems, } @@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 { return qty * price } -func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 { - if totalChickinQty > 0 { - return totalPrice / totalChickinQty +func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 { + if totalActualPopulation > 0 { + return totalPrice / totalActualPopulation } return 0 } diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index 494f2736..c89e6125 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -13,6 +13,7 @@ import ( rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" @@ -31,11 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) + recordingRepo := rRecording.NewRecordingRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 29001149..1cb26948 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -15,6 +15,7 @@ import ( marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -47,9 +48,10 @@ type closingService struct { ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository PurchaseRepo purchaseRepository.PurchaseRepository + RecordingRepo recordingRepository.RecordingRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -62,6 +64,7 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, PurchaseRepo: purchaseRepo, + RecordingRepo: recordingRepo, } } @@ -379,7 +382,14 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove totalChickinQty += chickin.UsageQty } - result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty) + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + totalActualPopulation := totalChickinQty - totalDepletion + + result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) return &result, nil } @@ -435,7 +445,12 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins) + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) + } + + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced) return &report, nil } From e52a02b1c0e90855f5ec35d1c5c0d9de4f8a21fd Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 11:30:55 +0700 Subject: [PATCH 12/31] Feat(BE-339): make reporting purchase per supplier with filterization --- .../repports/controllers/repport.controller.go | 8 ++++---- .../purchase_supplier.repository.go | 17 ++++++++--------- .../repports/validations/repport.validation.go | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 3e6c39d0..039854c8 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -106,8 +106,8 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { SupplierId: int64(ctx.QueryInt("supplier_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)), ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)), - DateFrom: ctx.Query("date_from", ""), - DateTo: ctx.Query("date_to", ""), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), SortBy: ctx.Query("sort_by", ""), FilterBy: ctx.Query("filter_by", ""), } @@ -126,8 +126,8 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { "supplier_id": query.SupplierId, "product_id": query.ProductId, "product_category_id": query.ProductCategoryId, - "date_from": query.DateFrom, - "date_to": query.DateTo, + "start_date": query.StartDate, + "end_date": query.EndDate, "sort_by": query.SortBy, "filter_by": query.FilterBy, } diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index cd282e8e..979623fc 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -26,7 +26,6 @@ func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { } func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { - // Tentukan kolom tanggal yang akan dipakai untuk filter dateColumn := "purchase_items.received_date" switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { case "po_date": @@ -60,14 +59,14 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("warehouses.area_id = ?", filters.AreaId) } - if filters.DateFrom != "" { - if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) } } - if filters.DateTo != "" { - if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) } } @@ -171,14 +170,14 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("warehouses.area_id = ?", filters.AreaId) } - if filters.DateFrom != "" { - if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil { + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) } } - if filters.DateTo != "" { - if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil { + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) } } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 942eeaa8..53ba22d7 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -35,8 +35,8 @@ type PurchaseSupplierQuery struct { SupplierId int64 `query:"supplier_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"` ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` - DateFrom string `query:"date_from" validate:"omitempty"` - DateTo string `query:"date_to" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty"` FilterBy string `query:"filter_by" validate:"omitempty"` } From d675b1e82651fba37d76ba3a33e0cd5bddacb6bc Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 18 Dec 2025 13:32:48 +0700 Subject: [PATCH 13/31] feat[BE-375]: get api closing data produksi --- .../controllers/closing.controller.go | 22 ++ internal/modules/closings/dto/closing.dto.go | 46 ++++ .../repositories/closing.repository.go | 164 +++++++++++++ internal/modules/closings/route.go | 1 + .../closings/services/closing.service.go | 218 ++++++++++++++++++ 5 files changed, 451 insertions(+) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index dc39a666..cd105a48 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -188,3 +188,25 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { Data: result, }) } + +func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error { + param := c.Params("projectFlockId") + + id, err := strconv.Atoi(param) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") + } + + result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved production data successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 1f1cb492..b3075776 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -58,6 +58,52 @@ type ClosingSummaryDTO struct { StatusClosing string `json:"closing_status"` } +type ClosingPurchaseDTO struct { + InitialPopulation int `json:"initial_population"` + ClaimCulling int `json:"claim_culling"` + FinalPopulation int `json:"final_population"` + FeedIn float64 `json:"feed_in"` + FeedUsed float64 `json:"feed_used"` + FeedUsedPerHead float64 `json:"feed_used_per_head"` +} + +type ClosingSalesDTO struct { + SalesPopulation int `json:"sales_population"` + SalesWeight float64 `json:"sales_weight"` + AverageWeight float64 `json:"average_weight"` + AverageSellingPrice float64 `json:"average_selling_price"` +} + +type ClosingEggSalesDTO struct { + EggPieces int `json:"egg_pieces"` + EggMassKg float64 `json:"egg_mass_kg"` + AverageEggWeightKg float64 `json:"average_egg_weight_kg"` + AverageSellingPrice float64 `json:"average_selling_price"` +} + +type ClosingPerformanceDTO struct { + Depletion float64 `json:"depletion"` + Age float64 `json:"age"` + MortalityStd float64 `json:"mortality_std"` + MortalityAct float64 `json:"mortality_act"` + DeffMortality float64 `json:"deff_mortality"` + FcrStd float64 `json:"fcr_std"` + FcrAct float64 `json:"fcr_act"` + DeffFcr float64 `json:"deff_fcr"` + Adg float64 `json:"adg"` +} + +type ClosingSalesGroupDTO struct { + ChickenProduction ClosingSalesDTO `json:"chicken_production"` + EggProduction ClosingEggSalesDTO `json:"egg_production"` +} + +type ClosingProductionReportDTO struct { + Purchase ClosingPurchaseDTO `json:"purchase"` + Sales ClosingSalesGroupDTO `json:"sales"` + Performance ClosingPerformanceDTO `json:"performance"` +} + func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO { history := project.KandangHistory diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index fe555378..186f48a2 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -9,12 +9,19 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) + SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) + SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) + SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) + GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) } type ClosingRepositoryImpl struct { @@ -102,6 +109,163 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak return rows, totalResults, nil } +func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, nil + } + + var purchaseAgg struct { + TotalIn float64 `gorm:"column:total_in"` + } + + err := r.DB().WithContext(ctx). + Table("purchase_items pi"). + Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). + Where("f.name = ?", "PAKAN"). + Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). + Scan(&purchaseAgg).Error + if err != nil { + return 0, 0, err + } + + var usageAgg struct { + TotalUsed float64 `gorm:"column:total_used"` + } + + err = r.DB().WithContext(ctx). + Table("recording_stocks rs"). + Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", "PAKAN"). + Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). + Scan(&usageAgg).Error + if err != nil { + return 0, 0, err + } + + return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil +} + +func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var agg struct { + Total float64 `gorm:"column:total_culling"` + } + + err := r.DB().WithContext(ctx). + Table("recording_depletions rd"). + Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", utils.FlagAyamCulling). + Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.Total, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, nil + } + + var agg struct { + TotalQty float64 `gorm:"column:total_qty"` + } + + err := r.DB().WithContext(ctx). + Table("recording_eggs re"). + Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(re.qty), 0) AS total_qty"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.TotalQty, nil +} + +func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) { + if fcrID == 0 { + return []entity.FcrStandard{}, nil + } + + var standards []entity.FcrStandard + if err := r.DB().WithContext(ctx). + Where("fcr_id = ?", fcrID). + Order("weight ASC"). + Find(&standards).Error; err != nil { + return nil, err + } + + return standards, nil +} + const ( sapronakIncomingPurchasesSQL = ` SELECT diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 4d142f44..6de2dc0b 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,4 +25,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + route.Get("/:projectFlockId/data-produksi", ctrl.GetClosingDataProduksi) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index cfc22948..f8957a99 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "math" "strconv" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -30,6 +31,7 @@ type ClosingService interface { GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) + GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) } @@ -379,3 +381,219 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove return &result, nil } + +func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + if err != nil { + s.Log.Errorf("Failed get project flock %d for closing data produksi: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + var population float64 + for _, history := range project.KandangHistory { + for _, chickin := range history.Chickins { + population += chickin.UsageQty + chickin.PendingUsageQty + } + } + + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") + } + + feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") + } + + claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data") + } + + finalPopulation := population - claimCulling + + var standards []entity.FcrStandard + if project.FcrId > 0 { + standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId) + if err != nil { + s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") + } + } + // masih dummy, karena tab penjualan agenya masih dummy juga + age := 1.0 + + feedUsedPerHead := 0.0 + if population > 0 { + feedUsedPerHead = feedUsed / population + } + + purchase := dto.ClosingPurchaseDTO{ + InitialPopulation: int(population), + ClaimCulling: int(claimCulling), + FinalPopulation: int(finalPopulation), + FeedIn: feedIn, + FeedUsed: feedUsed, + FeedUsedPerHead: feedUsedPerHead, + } + + chickenFlagNames := []string{string(utils.FlagPullet)} + chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data") + } + + var chickenAverageWeight float64 + if chickenSalesQty > 0 { + chickenAverageWeight = chickenSalesWeight / chickenSalesQty + } + + var chickenAverageSellingPrice float64 + if chickenSalesWeight > 0 { + chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight + } + + eggFlagNames := []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + } + eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") + } + + var averageEggWeight float64 + if eggSalesQty > 0 { + averageEggWeight = eggSalesWeight / eggSalesQty + } + + var averageEggSellingPrice float64 + if eggSalesWeight > 0 { + averageEggSellingPrice = eggSalesPrice / eggSalesWeight + } + + chickenSales := dto.ClosingSalesDTO{ + SalesPopulation: int(chickenSalesQty), + SalesWeight: chickenSalesWeight, + AverageWeight: chickenAverageWeight, + AverageSellingPrice: chickenAverageSellingPrice, + } + + eggSales := dto.ClosingEggSalesDTO{ + EggPieces: int(eggSalesQty), + EggMassKg: eggSalesWeight, + AverageEggWeightKg: averageEggWeight, + AverageSellingPrice: averageEggSellingPrice, + } + + harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") + } + + chickenDepletion := population - chickenSalesQty + if chickenDepletion < 0 { + chickenDepletion = 0 + } + eggDepletion := harvestEggQty - eggSalesQty + if eggDepletion < 0 { + eggDepletion = 0 + } + + chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) + eggPerformance := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + + sales := dto.ClosingSalesGroupDTO{ + ChickenProduction: chickenSales, + EggProduction: eggSales, + } + + performance := dto.ClosingPerformanceDTO{ + Depletion: chickenPerformance.Depletion, + Age: age, + MortalityStd: chickenPerformance.MortalityStd, + MortalityAct: chickenPerformance.MortalityAct, + DeffMortality: chickenPerformance.DeffMortality, + FcrStd: eggPerformance.FcrStd, + FcrAct: eggPerformance.FcrAct, + DeffFcr: eggPerformance.DeffFcr, + Adg: eggPerformance.Adg, + } + + result := dto.ClosingProductionReportDTO{ + Purchase: purchase, + Sales: sales, + Performance: performance, + } + + return &result, nil +} + +func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { + mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) + + fcrAct := 0.0 + if totalWeight > 0 { + fcrAct = feedUsed / totalWeight + } + + mortalityAct := 0.0 + if basePopulation > 0 { + mortalityAct = (depletion / basePopulation) * 100 + } + + deffMortality := mortalityStd - mortalityAct + deffFcr := fcrStd - fcrAct + + adg := 0.0 + if age > 0 { + adg = averageWeight / age + } + + return dto.ClosingPerformanceDTO{ + Depletion: depletion, + Age: age, + MortalityStd: mortalityStd, + MortalityAct: mortalityAct, + DeffMortality: deffMortality, + FcrStd: fcrStd, + FcrAct: fcrAct, + DeffFcr: deffFcr, + Adg: adg, + } +} + +func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) { + if len(standards) == 0 || averageWeight <= 0 { + return 0, 0 + } + + closest := standards[0] + minDiff := math.Abs(closest.Weight - averageWeight) + for _, std := range standards[1:] { + diff := math.Abs(std.Weight - averageWeight) + if diff < minDiff { + minDiff = diff + closest = std + } + } + + return closest.Mortality, closest.FcrNumber +} From f2df7f4847fdc81856ae03e9af6178813c19ce46 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 14:49:48 +0700 Subject: [PATCH 14/31] feat[BE]: add overhead and ekspedisi items to profit loss report; include total depletion in closing report calculation --- .../closings/dto/closingKeuangan.dto.go | 78 ++++++++++++------- .../closings/services/closing.service.go | 8 +- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 13e7c196..978c0b60 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -60,6 +60,8 @@ type PLSummaryGroup struct { type ProfitLossData struct { Penjualan []PLItem `json:"penjualan"` Pembelian []PLItem `json:"pembelian"` + Overhead PLItem `json:"overhead"` + Ekspedisi PLItem `json:"ekspedisi"` Summary PLSummaryGroup `json:"summary"` } @@ -167,15 +169,13 @@ func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entiti ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced) - if ekspedisiAmount > 0 { - items = append(items, HppItem{ - Type: "Beban Ekspedisi", - Comparison: ToComparison( - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization - ), - }) - } + items = append(items, HppItem{ + Type: "Beban Ekspedisi", + Comparison: ToComparison( + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ), + }) return HppGroup{ GroupName: "HPP dan Bahan Baku", @@ -248,19 +248,28 @@ func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { } func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { + items := []PLItem{} + // Categorize deliveries by sales type based on Product flags categorized := categorizeDeliveriesBySalesType(deliveryProducts) - items := []PLItem{} + if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { + // For LAYING: show both Penjualan Ayam Besar and Penjualan Telur (even if 0) + ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) + telurAmount := sumDeliveriesByCategory(categorized["Penjualan Telur"]) - // Process each sales category - for salesType, deliveries := range categorized { - amount := sumDeliveriesByCategory(deliveries) + // Penjualan Ayam Besar + rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) - // Use totalPopulation and totalWeightSold for per-unit calculations - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold) - - items = append(items, ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))) + // Penjualan Telur + rpPerBird, rpPerKg = calculatePerUnitMetrics(telurAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Telur", ToFinancialMetrics(rpPerBird, rpPerKg, telurAmount))) + } else { + // For GROWING: show only Penjualan Ayam Besar + ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) + rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) + items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) } return items @@ -278,7 +287,7 @@ func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.Proj rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Harga Pokok Penjualan (HPP)", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), + ToPLItem("Pembelian Sapronak", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), } } @@ -301,20 +310,21 @@ func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulatio func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) - totalOverhead, _ := sumPLItems(overheadItems) - totalEkspedisi, _ := sumPLItems(ekspedisiItems) + 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 - 0.0 + netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird return PLSummaryGroup{ GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(0, 0, totalOtherExpenses)), + SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), } } @@ -322,9 +332,15 @@ func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiIt func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + // Get total overhead and ekspedisi as single items + totalOverhead := aggregatePLItems(overheadItems, "Pengeluaran Overhead") + totalEkspedisi := aggregatePLItems(ekspedisiItems, "Beban Ekspedisi") + return ProfitLossData{ Penjualan: penjualanItems, Pembelian: pembelianItems, + Overhead: totalOverhead, + Ekspedisi: totalEkspedisi, Summary: summary, } } @@ -335,6 +351,11 @@ func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedis } } +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, @@ -342,9 +363,7 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -// === MAIN BUILDER === - -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced float64) ReportResponse { +func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced, totalDepletion float64) ReportResponse { var totalPopulation float64 var totalWeightSold float64 @@ -356,13 +375,16 @@ func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entiti totalWeightSold += delivery.TotalWeight } + // Calculate actual population (chickin - depletion) for cost allocation + actualPopulation := totalPopulation - totalDepletion + // Use totalWeightProduced for HPP calculation (not totalWeightSold) hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, totalPopulation, totalWeightProduced) - overheadItems := ToOverheadItems(budgets, realizations, totalPopulation, totalWeightProduced) - ekspedisiItems := ToEkspedisiItems(realizations, totalPopulation, totalWeightProduced) + pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, actualPopulation, totalWeightProduced) + overheadItems := ToOverheadItems(budgets, realizations, actualPopulation, totalWeightProduced) + ekspedisiItems := ToEkspedisiItems(realizations, actualPopulation, totalWeightProduced) plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) return ToReportResponse(hppSection, plSection) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 1cb26948..84b14ace 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -450,7 +450,13 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced) + // Fetch depletion data to calculate actual population for cost allocation + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced, totalDepletion) return &report, nil } From 9e0b4be4dd60b66a3f64890f18d6911163c4c768 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 14:52:51 +0700 Subject: [PATCH 15/31] feat[BE]: add flags to product seeds for better categorization --- internal/database/seed/seeder.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 8da408ca..bb4090bb 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -588,6 +588,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamAfkir}, }, { Name: "Ayam Mati", @@ -596,6 +597,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamMati}, }, { Name: "Ayam Culling", @@ -604,6 +606,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamCulling}, }, { Name: "Telur Konsumsi Baik", @@ -612,6 +615,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurUtuh}, }, { Name: "Telur Pecah", @@ -620,6 +624,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurPecah}, }, { Name: "281 SPECIAL STARTER", @@ -632,6 +637,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, + { + Name: "Ayam Layer", + Brand: "-", + Sku: "LYR0001", + Uom: "Ekor", + Category: "Pullet", + Price: 20000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagLayer}, + }, } for _, seed := range seeds { From c95f90f0b9d1060059f2ec5315a614a323406f8c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 15:03:37 +0700 Subject: [PATCH 16/31] Refactor[BE]: refactor expense category handling to use constants for BOP and NON-BOP --- .../expenses/services/expense.service.go | 16 ++++++++-------- internal/utils/constant.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index dbfb00c2..24ba4f2e 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -213,7 +213,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen var projectFlockKandangId *uint64 - if req.Category == "BOP" { + if req.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -230,10 +230,10 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen nonstockId := costItem.NonstockID var kandangId *uint64 - if req.Category == "NON-BOP" { + if req.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if req.Category == "BOP" { + } else if req.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } @@ -385,7 +385,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if categoryChanged { - if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { + if currentExpense.Category == string(utils.ExpenseCategoryBOP) && newCategory == string(utils.ExpenseCategoryNonBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -400,7 +400,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") } } - } else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { + } else if currentExpense.Category == string(utils.ExpenseCategoryNonBOP) && newCategory == string(utils.ExpenseCategoryBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -457,7 +457,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) for _, expenseNonstock := range *req.ExpenseNonstocks { var projectFlockKandangId *uint64 - if updatedExpense.Category == "BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -480,10 +480,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } var kandangId *uint64 - if updatedExpense.Category == "NON-BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if updatedExpense.Category == "BOP" { + } else if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d4f6ec02..85b33f9b 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -135,6 +135,17 @@ const ( SupplierCategorySapronak SupplierCategory = "SAPRONAK" ) +// ------------------------------------------------------------------- +// ExpenseCategory +// ------------------------------------------------------------------- + +type ExpenseCategory string + +const ( + ExpenseCategoryBOP ExpenseCategory = "BOP" + ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP" +) + // ------------------------------------------------------------------- // Kandang Status // ------------------------------------------------------------------- @@ -429,6 +440,14 @@ func IsValidSupplierCategory(v string) bool { return false } +func IsValidExpenseCategory(v string) bool { + switch ExpenseCategory(v) { + case ExpenseCategoryBOP, ExpenseCategoryNonBOP: + return true + } + return false +} + // example use // Recording helper From 047162699e385a295be9aca1417b18557956d2a9 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Thu, 18 Dec 2025 15:25:15 +0700 Subject: [PATCH 17/31] adjust response api closing data produksi --- internal/modules/closings/dto/closing.dto.go | 12 +- .../closings/services/closing.service.go | 120 ++++++++++-------- 2 files changed, 75 insertions(+), 57 deletions(-) diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index b3075776..429495b7 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -71,31 +71,31 @@ type ClosingSalesDTO struct { SalesPopulation int `json:"sales_population"` SalesWeight float64 `json:"sales_weight"` AverageWeight float64 `json:"average_weight"` - AverageSellingPrice float64 `json:"average_selling_price"` + AverageSellingPrice float64 `json:"chicken_average_selling_price"` } type ClosingEggSalesDTO struct { EggPieces int `json:"egg_pieces"` EggMassKg float64 `json:"egg_mass_kg"` AverageEggWeightKg float64 `json:"average_egg_weight_kg"` - AverageSellingPrice float64 `json:"average_selling_price"` + AverageSellingPrice float64 `json:"egg_average_selling_price"` } type ClosingPerformanceDTO struct { Depletion float64 `json:"depletion"` - Age float64 `json:"age"` + Age float64 `json:"age_day"` MortalityStd float64 `json:"mortality_std"` MortalityAct float64 `json:"mortality_act"` DeffMortality float64 `json:"deff_mortality"` FcrStd float64 `json:"fcr_std"` FcrAct float64 `json:"fcr_act"` DeffFcr float64 `json:"deff_fcr"` - Adg float64 `json:"adg"` + Awg float64 `json:"awg"` } type ClosingSalesGroupDTO struct { - ChickenProduction ClosingSalesDTO `json:"chicken_production"` - EggProduction ClosingEggSalesDTO `json:"egg_production"` + Chicken ClosingSalesDTO `json:"chicken"` + Egg *ClosingEggSalesDTO `json:"egg,omitempty"` } type ClosingProductionReportDTO struct { diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index f8957a99..e5479f35 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -5,6 +5,7 @@ import ( "errors" "math" "strconv" + "strings" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -403,6 +404,8 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint } } + isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) @@ -431,7 +434,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") } } - // masih dummy, karena tab penjualan agenya masih dummy juga + // masih dummy, karena tab penjualan agenya masih dummy age := 1.0 feedUsedPerHead := 0.0 @@ -465,29 +468,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight } - eggFlagNames := []string{ - string(utils.FlagTelur), - string(utils.FlagTelurUtuh), - string(utils.FlagTelurPecah), - string(utils.FlagTelurPutih), - string(utils.FlagTelurRetak), - } - eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) - if err != nil { - s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") - } - - var averageEggWeight float64 - if eggSalesQty > 0 { - averageEggWeight = eggSalesWeight / eggSalesQty - } - - var averageEggSellingPrice float64 - if eggSalesWeight > 0 { - averageEggSellingPrice = eggSalesPrice / eggSalesWeight - } - chickenSales := dto.ClosingSalesDTO{ SalesPopulation: int(chickenSalesQty), SalesWeight: chickenSalesWeight, @@ -495,34 +475,65 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint AverageSellingPrice: chickenAverageSellingPrice, } - eggSales := dto.ClosingEggSalesDTO{ - EggPieces: int(eggSalesQty), - EggMassKg: eggSalesWeight, - AverageEggWeightKg: averageEggWeight, - AverageSellingPrice: averageEggSellingPrice, - } - - harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) - if err != nil { - s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") - } - chickenDepletion := population - chickenSalesQty if chickenDepletion < 0 { chickenDepletion = 0 } - eggDepletion := harvestEggQty - eggSalesQty - if eggDepletion < 0 { - eggDepletion = 0 - } chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) - eggPerformance := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + + var eggSales *dto.ClosingEggSalesDTO + var eggPerformance *dto.ClosingPerformanceDTO + if !isGrowing { + eggFlagNames := []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + } + + eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") + } + + var averageEggWeight float64 + if eggSalesQty > 0 { + averageEggWeight = eggSalesWeight / eggSalesQty + } + + var averageEggSellingPrice float64 + if eggSalesWeight > 0 { + averageEggSellingPrice = eggSalesPrice / eggSalesWeight + } + + eggSales = &dto.ClosingEggSalesDTO{ + EggPieces: int(eggSalesQty), + EggMassKg: eggSalesWeight, + AverageEggWeightKg: averageEggWeight, + AverageSellingPrice: averageEggSellingPrice, + } + + harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") + } + + eggDepletion := harvestEggQty - eggSalesQty + if eggDepletion < 0 { + eggDepletion = 0 + } + + eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + eggPerformance = &eggPerf + } sales := dto.ClosingSalesGroupDTO{ - ChickenProduction: chickenSales, - EggProduction: eggSales, + Chicken: chickenSales, + Egg: eggSales, } performance := dto.ClosingPerformanceDTO{ @@ -531,10 +542,17 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint MortalityStd: chickenPerformance.MortalityStd, MortalityAct: chickenPerformance.MortalityAct, DeffMortality: chickenPerformance.DeffMortality, - FcrStd: eggPerformance.FcrStd, - FcrAct: eggPerformance.FcrAct, - DeffFcr: eggPerformance.DeffFcr, - Adg: eggPerformance.Adg, + } + if eggPerformance != nil { + performance.FcrStd = eggPerformance.FcrStd + performance.FcrAct = eggPerformance.FcrAct + performance.DeffFcr = eggPerformance.DeffFcr + performance.Awg = eggPerformance.Awg + } else { + performance.FcrStd = chickenPerformance.FcrStd + performance.FcrAct = chickenPerformance.FcrAct + performance.DeffFcr = chickenPerformance.DeffFcr + performance.Awg = chickenPerformance.Awg } result := dto.ClosingProductionReportDTO{ @@ -562,9 +580,9 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul deffMortality := mortalityStd - mortalityAct deffFcr := fcrStd - fcrAct - adg := 0.0 + awg := 0.0 if age > 0 { - adg = averageWeight / age + awg = averageWeight / age } return dto.ClosingPerformanceDTO{ @@ -576,7 +594,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul FcrStd: fcrStd, FcrAct: fcrAct, DeffFcr: deffFcr, - Adg: adg, + Awg: awg, } } From 14a4d9e944374478eb6e74708734069b0a029763 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 16:02:57 +0700 Subject: [PATCH 18/31] Feat(BE-334):Fixing dto closing hpp expedisi --- internal/modules/closings/dto/closingExpedition.dto.go | 4 ---- .../dto/{sapronak.dto.go => closingSapronak.dto.go} | 0 .../modules/closings/repositories/closing.repository.go | 5 +---- internal/modules/closings/services/closing.service.go | 8 -------- 4 files changed, 1 insertion(+), 16 deletions(-) rename internal/modules/closings/dto/{sapronak.dto.go => closingSapronak.dto.go} (100%) diff --git a/internal/modules/closings/dto/closingExpedition.dto.go b/internal/modules/closings/dto/closingExpedition.dto.go index f1b8628b..5f8a09d4 100644 --- a/internal/modules/closings/dto/closingExpedition.dto.go +++ b/internal/modules/closings/dto/closingExpedition.dto.go @@ -3,10 +3,7 @@ package dto // ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor. type ExpeditionCostItemDTO struct { Id uint64 `json:"id"` - ExpeditionVendorID uint64 `json:"expedition_vendor_id"` ExpeditionVendorName string `json:"expedition_vendor_name"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` HPPAmount float64 `json:"hpp_amount"` } @@ -15,4 +12,3 @@ type ExpeditionHPPDTO struct { ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"` TotalHPPAmount float64 `json:"total_hpp_amount"` } - diff --git a/internal/modules/closings/dto/sapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go similarity index 100% rename from internal/modules/closings/dto/sapronak.dto.go rename to internal/modules/closings/dto/closingSapronak.dto.go diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 14854430..0214d739 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -55,9 +55,7 @@ type SapronakRow struct { } type ExpeditionHPPRow struct { - SupplierID uint64 `gorm:"column:supplier_id"` SupplierName string `gorm:"column:supplier_name"` - Qty float64 `gorm:"column:qty"` TotalAmount float64 `gorm:"column:total_amount"` } @@ -147,7 +145,6 @@ func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlo Select( "e.supplier_id AS supplier_id, " + "s.name AS supplier_name, " + - "SUM(er.qty) AS qty, " + "SUM(er.qty * er.price) AS total_amount", ). Group("e.supplier_id, s.name"). @@ -645,4 +642,4 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand return fmt.Sprintf("TRF-%d", row.ID) }) return in, out, nil -} \ No newline at end of file +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index d6e24e7f..afba0a9d 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -398,17 +398,9 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj var totalHPP float64 for idx, row := range rows { - unitPrice := 0.0 - if row.Qty > 0 { - unitPrice = row.TotalAmount / row.Qty - } - expeditionCosts = append(expeditionCosts, dto.ExpeditionCostItemDTO{ Id: uint64(idx + 1), - ExpeditionVendorID: row.SupplierID, ExpeditionVendorName: row.SupplierName, - Qty: row.Qty, - UnitPrice: unitPrice, HPPAmount: row.TotalAmount, }) From f5c80fa560aafb6de4f5c22c3290e5e9d0e4fe00 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 16:21:46 +0700 Subject: [PATCH 19/31] Feat(BE-339):Fixing dto reporting per supplier --- .../repports/dto/repportPurchase.dto.go | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go index 60fd0fee..830a076f 100644 --- a/internal/modules/repports/dto/repportPurchase.dto.go +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -1,6 +1,7 @@ package dto import ( + "math" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -26,10 +27,12 @@ type PurchaseSupplierRowDTO struct { } type PurchaseSupplierSummaryDTO struct { - TotalQty float64 `json:"total_qty"` - TotalPurchaseValue float64 `json:"total_purchase_value"` - TotalTransportValue float64 `json:"total_transport_value"` - TotalAmount float64 `json:"total_amount"` + TotalQty float64 `json:"total_qty"` + TotalPurchaseValue float64 `json:"total_purchase_value"` + TotalTransportValue float64 `json:"total_transport_value"` + TotalAmount float64 `json:"total_amount"` + TotalUnitPrice float64 `json:"total_unit_price"` + TotalTransportUnitPrice float64 `json:"total_transport_unit_price"` } type PurchaseSupplierDTO struct { @@ -119,6 +122,11 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem rows := make([]PurchaseSupplierRowDTO, 0, len(items)) summary := PurchaseSupplierSummaryDTO{} + var unitPriceSum float64 + var unitPriceCount int + var transportUnitPriceSum float64 + var transportUnitPriceCount int + for i := range items { row := ToPurchaseSupplierRowDTO(&items[i]) rows = append(rows, row) @@ -127,6 +135,20 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem summary.TotalPurchaseValue += row.PurchaseValue summary.TotalTransportValue += row.TransportValue summary.TotalAmount += row.TotalAmount + + unitPriceSum += row.UnitPrice + unitPriceCount++ + + transportUnitPriceSum += row.TransportUnitPrice + transportUnitPriceCount++ + } + + if unitPriceCount > 0 { + summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount)) + } + + if transportUnitPriceCount > 0 { + summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount)) } return PurchaseSupplierDTO{ @@ -135,4 +157,3 @@ func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem Summary: summary, } } - From cb076d92ace5a7e3eb666dd783a9a83573bfc6b6 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 18 Dec 2025 16:41:56 +0700 Subject: [PATCH 20/31] Feat(BE-339):Fixing dto reporting per supplier, and adjust limit --- internal/modules/repports/validations/repport.validation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 53ba22d7..a69e7716 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -30,7 +30,7 @@ type MarketingQuery struct { type PurchaseSupplierQuery struct { Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` AreaId int64 `query:"area_id" validate:"omitempty"` SupplierId int64 `query:"supplier_id" validate:"omitempty"` ProductId int64 `query:"product_id" validate:"omitempty"` From e551995c66c86ebeb3de12bee755f2df975826c9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 18 Dec 2025 17:56:18 +0700 Subject: [PATCH 21/31] feat[BE-384]: enhance reporting by adding chickin quantity and egg production weight calculations; refactor HPP calculations to consider product categories --- .../salesorder_delivery_product.repository.go | 1 + .../project_chickin.repository.go | 12 ++ .../repositories/recording.repository.go | 22 ++- .../repports/dto/repportMarketing.dto.go | 100 ++++++++++---- internal/modules/repports/module.go | 4 +- .../repports/services/repport.service.go | 128 +++++++----------- 6 files changed, 157 insertions(+), 110 deletions(-) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index b908681e..ba2c1133 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -90,6 +90,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). + Preload("ProductWarehouse.Product.Flags"). Preload("ProductWarehouse.Warehouse"). Preload("ProductWarehouse.ProjectFlockKandang"). Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock") diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index bef062f5..43cafaac 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -15,6 +15,7 @@ type ProjectChickinRepository interface { GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) } type ChickinRepositoryImpl struct { @@ -90,3 +91,14 @@ func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(c } return total, nil } + +func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.db.WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 4a7e627c..85c9a7fe 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -48,6 +48,7 @@ type RecordingRepository interface { GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) + GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) } type RecordingRepositoryImpl struct { @@ -371,28 +372,23 @@ func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx return 0, 0, nil } - // Get total chickin quantity for this ProjectFlock totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID) if err != nil { return 0, 0, err } - // Get total depletion for this ProjectFlock totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) if err != nil { return 0, 0, err } - // Calculate actual quantity produced actualQty := totalChickinQty - totalDepletion - // Get latest average weight from RecordingBW avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) if err != nil { return 0, 0, err } - // Calculate total weight totalWeight = actualQty * avgWeight return totalWeight, actualQty, nil @@ -434,6 +430,22 @@ func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context return result, err } +func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_eggs"). + Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000"). + Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + func nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index deadf3b8..9c026590 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,14 +1,15 @@ package dto import ( - "fmt" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type RepportMarketingItemDTO struct { @@ -45,7 +46,7 @@ type RepportMarketingResponseDTO struct { Total *Summary `json:"total,omitempty"` } -func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO { soDate := time.Time{} agingDays := 0 if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { @@ -58,11 +59,17 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK realizationDate = *mdp.DeliveryDate } - doNumber := generateDoNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) + doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) totalWeightKg := mdp.Qty * mdp.AvgWeight salesAmount := totalWeightKg * mdp.UnitPrice - hppAmount := totalWeightKg * hppPricePerKg + + var hpp float64 + var hppAmount float64 + if isProductEligibleForHpp(mdp, category) { + hpp = hppPricePerKg + hppAmount = totalWeightKg * hppPricePerKg + } item := RepportMarketingItemDTO{ ID: int(mdp.Id), @@ -70,12 +77,12 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK RealizationDate: realizationDate, AgingDays: agingDays, DoNumber: doNumber, - MarketingType: "ayam", + MarketingType: getMarketingType(mdp), Qty: mdp.Qty, AverageWeightKg: mdp.AvgWeight, TotalWeightKg: totalWeightKg, SalesPricePerKg: mdp.UnitPrice, - HppPricePerKg: hppPricePerKg, + HppPricePerKg: hpp, SalesAmount: salesAmount, HppAmount: hppAmount, } @@ -105,10 +112,10 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK return item } -func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO { items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { - items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg)) + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category)) } return items } @@ -117,23 +124,72 @@ func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct items := make([]RepportMarketingItemDTO, 0, len(mdps)) for _, mdp := range mdps { hppPerKg := float64(0) + category := "" if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { hppPerKg = hpp } + category = projectFlockKandang.ProjectFlock.Category } - items = append(items, ToRepportMarketingItemDTO(mdp, hppPerKg)) + + item := ToRepportMarketingItemDTO(mdp, hppPerKg, category) + items = append(items, item) } return items } -func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *Summary { +func getMarketingType(mdp entity.MarketingDeliveryProduct) string { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if hasAyam { + return "ayam" + } + if hasTelur { + return "telur" + } + return "trading" +} + +func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) { + if len(flags) == 0 { + return false, false + } + + for _, flag := range flags { + ft := utils.FlagType(flag.Name) + + if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati || + ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer { + hasAyam = true + } + + if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah || + ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak { + hasTelur = true + } + } + + return hasAyam, hasTelur +} + +func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + return hasAyam + } + + return hasAyam || hasTelur +} + +func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary { if len(mdps) == 0 { return nil } totalQty := 0 totalWeightKg := 0.0 + totalEligibleWeightKg := 0.0 totalSalesAmount := int64(0) totalHppAmount := int64(0) @@ -142,12 +198,16 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S totalQty += int(mdp.Qty) totalWeightKg += calculatedTotalWeight totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) - totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + + if isProductEligibleForHpp(mdp, category) { + totalEligibleWeightKg += calculatedTotalWeight + totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + } } totalHppPricePerKg := float64(0) - if totalWeightKg > 0 { - totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg + if totalEligibleWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg } return &Summary{ @@ -159,14 +219,6 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) *S } } -func generateDoNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string { - dateStr := "" - if deliveryDate != nil { - dateStr = deliveryDate.Format("20060102") - } - return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId) -} - func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { if len(items) == 0 { return nil @@ -198,9 +250,9 @@ func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { } } -func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { - items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) - total := ToSummary(mdps, hppPricePerKg) +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category) + total := ToSummary(mdps, hppPricePerKg, category) return RepportMarketingResponseDTO{ Items: items, diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index f347ab69..95d77dc1 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -11,6 +11,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" ) @@ -22,11 +23,12 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) purchaseRepository := purchaseRepo.NewPurchaseRepository(db) + chickinRepository := chickinRepo.NewChickinRepository(db) recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, recordingRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc) RepportRoutes(router, repportService) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 5458a28d..7513cbb1 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -3,7 +3,6 @@ package service import ( "context" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -12,6 +11,7 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" @@ -32,17 +32,19 @@ type repportService struct { ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository + ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, + ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, } @@ -98,74 +100,63 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) - hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) - items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) + projectFlockIDMap := make(map[uint]bool) + hppMap := make(map[uint]float64) + for _, dp := range deliveryProducts { + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + projectFlockID := projectFlockKandang.ProjectFlockId + if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] { + projectFlockIDMap[projectFlockID] = true + + category := projectFlockKandang.ProjectFlock.Category + hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category) + hppMap[projectFlockID] = hppPerKg + } + } + } + + items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) return items, total, nil } -func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint { - projectFlockIDMap := make(map[uint]bool) - projectFlockIDs := make([]uint, 0) - - for _, dp := range deliveryProducts { - if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] { - projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId) - projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true - } - } - } - - return projectFlockIDs -} - -func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { - hppMap := make(map[uint]float64) - for _, projectFlockID := range projectFlockIDs { - category := s.getProjectFlockCategory(deliveryProducts, projectFlockID) - hppPerKg := s.calculateHppByCategory(ctx, category, projectFlockID, deliveryProducts) - hppMap[projectFlockID] = hppPerKg - } - return hppMap -} - -func (s *repportService) calculateHppByCategory(ctx context.Context, category string, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { - switch utils.ProjectFlockCategory(category) { - case utils.ProjectFlockCategoryGrowing: - return s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) - case utils.ProjectFlockCategoryLaying: - return 0 - default: +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { + totalCost := s.getTotalProjectCost(ctx, projectFlockID) + if totalCost == 0 { return 0 } -} -func (s *repportService) getProjectFlockCategory(deliveryProducts []entity.MarketingDeliveryProduct, projectFlockID uint) string { - for _, dp := range deliveryProducts { - if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { - if projectFlockKandang.ProjectFlockId == projectFlockID { - return projectFlockKandang.ProjectFlock.Category - } - } + chickinQty, _ := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + depletion, _ := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + avgWeight, _ := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + + var totalWeight float64 + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + totalWeight = (chickinQty - depletion) * avgWeight + } else { + eggWeight, _ := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + totalWeight = (chickinQty-depletion)*avgWeight + eggWeight } - return "" + + if totalWeight == 0 { + return 0 + } + return totalCost / totalWeight } -func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { +func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { if projectFlockID == 0 { return 0 } - purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) + purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) } - costPurchase := float64(0) - for _, item := range purchaseItems { - costPurchase += item.TotalPrice + cost := float64(0) + for _, p := range purchases { + cost += p.TotalPrice } realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) @@ -173,34 +164,11 @@ func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFloc s.Log.Warnf("GetByProjectFlockID error: %v", err) } - costBop := float64(0) - for _, realization := range realizations { - cost := realization.Price * realization.Qty - category := "" - if realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil { - category = realization.ExpenseNonstock.Expense.Category - } - - if category == "BOP" { - costBop += cost + for _, r := range realizations { + if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && + r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { + cost += r.Price * r.Qty } } - - totalActualCost := costPurchase + costBop - - if totalActualCost == 0 { - return 0 - } - - totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(ctx, projectFlockID) - if err != nil { - s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) - } - - if totalWeightProduced == 0 { - return 0 - } - - hppPerKg := totalActualCost / totalWeightProduced - return hppPerKg + return cost } From 207382b3b0f5842478cd9d52e4373a61873f3785 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Fri, 19 Dec 2025 07:05:11 +0700 Subject: [PATCH 22/31] fix get all inventory product stock --- .../product-stocks/services/product-stock.service.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index a0765d84..11475109 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -64,11 +64,18 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e offset := (params.Page - 1) * params.Limit productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + WHERE pw.product_id = products.id + AND pw.qty > 0 + )`) + db = s.withRelations(db) if params.Search != "" { - return db.Where("name ILIKE ?", "%"+params.Search+"%") + db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + return db.Order("products.created_at DESC").Order("products.updated_at DESC") }) if err != nil { From fa6d82b79a0aca02c8a122736ba25f4f33297e3e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 19 Dec 2025 08:30:05 +0700 Subject: [PATCH 23/31] feat[BE-384]: enhance closing reports by introducing calculation context and improving data handling; refactor related functions for better clarity and maintainability --- .../closings/dto/closingKeuangan.dto.go | 305 ++++++++++-------- .../closings/dto/closingMarketing.dto.go | 20 +- .../closings/services/closing.service.go | 15 +- .../chickins/services/chickin.service.go | 29 +- .../services/project_flock_kandang.service.go | 15 +- .../repositories/recording.repository.go | 2 +- .../repositories/purchase.repository.go | 26 +- .../repports/services/repport.service.go | 41 ++- 8 files changed, 286 insertions(+), 167 deletions(-) diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 978c0b60..90dda2a9 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -1,13 +1,58 @@ package dto import ( + "slices" "strings" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) +// === CONSTANTS === +const ( + HPPGroupPengeluaran = "HPP dan Pengeluaran" + HPPGroupBahanBaku = "HPP dan Bahan Baku" + HPPLabelOverhead = "Pengeluaran Overhead" + HPPLabelEkspedisi = "Beban Ekspedisi" + HPPSummaryLabel = "HPP" + + PLSalesTypeChicken = "Penjualan Ayam Besar" + PLSalesTypeEgg = "Penjualan Telur" + + PLItemTypeSapronak = "Pembelian Sapronak" + PLItemTypeOverhead = "Pengeluaran Overhead" + PLItemTypeEkspedisi = "Beban Ekspedisi" + + PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO" + PLSummaryLabelSubTotal = "SUB TOTAL" + PLSummaryLabelNetProfit = "LABA RUGI NETTO" + + PurchaseLabelPrefix = "Pembelian " +) + +// === CONTEXT STRUCTS === + +type CalculationContext struct { + TotalPopulation float64 + TotalWeightProduced float64 + TotalDepletion float64 + TotalWeightSold float64 + ActualPopulation float64 +} + +type ClosingKeuanganInput struct { + ProjectFlockCategory string + PurchaseItems []entities.PurchaseItem + Budgets []entities.ProjectBudget + Realizations []entities.ExpenseRealization + DeliveryProducts []entities.MarketingDeliveryProduct + Chickins []entities.ProjectChickin + TotalWeightProduced float64 + TotalDepletion float64 +} + // === BASE METRICS === + type FinancialMetrics struct { RpPerBird float64 `json:"rp_per_bird"` RpPerKg float64 `json:"rp_per_kg"` @@ -20,6 +65,7 @@ type Comparison struct { } // === HPP PURCHASES PACKAGE === + type HppItem struct { Type string `json:"type"` Comparison @@ -41,6 +87,7 @@ type HppPurchasesSection struct { } // === PROFIT LOSS PACKAGE === + type PLItem struct { Type string `json:"type"` FinancialMetrics @@ -70,6 +117,7 @@ type ProfitLossSection struct { } // === RESPONSE DTO (ROOT) === + type ReportResponse struct { HppPurchases HppPurchasesSection `json:"hpp_purchases"` ProfitLoss ProfitLossSection `json:"profit_loss"` @@ -95,10 +143,10 @@ func ToComparison(budgeting, realization FinancialMetrics) Comparison { // === HPP PENGELUARAN (from Purchase Items) === func getFlagLabel(flagType utils.FlagType) string { - return "Pembelian " + string(flagType) + return PurchaseLabelPrefix + string(flagType) } -func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWeightProduced, totalPopulation float64) []HppItem { +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem { flags := []utils.FlagType{ utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, @@ -116,24 +164,15 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe for _, flag := range item.Product.Flags { flagType := utils.FlagType(flag.Name) - // Check if valid flag and not processed - isValid := false - for _, validFlag := range flags { - if validFlag == flagType { - isValid = true - break - } - } - - if isValid && !seenFlags[flagType] { + if slices.Contains(flags, flagType) && !seenFlags[flagType] { amount := sumPurchasesByFlag(purchaseItems, flagType) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) + 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), // Same for purchase + ToFinancialMetrics(rpPerBird, rpPerKg, amount), ), }) seenFlags[flagType] = true @@ -146,56 +185,61 @@ func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, totalWe // === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === -func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppGroup { - items := []HppItem{} +func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem { + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) - // Overhead: all budgets vs (all expenses EXCEPT ekspedisi) - budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) - realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, totalPopulation, totalWeightProduced) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) - - if budgetAmount > 0 || realizationAmount > 0 { - items = append(items, HppItem{ - Type: "Pengeluaran Overhead", - Comparison: ToComparison( - ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), - ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), - ), - }) + return HppItem{ + Type: HPPLabelOverhead, + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ), } +} - // Ekspedisi: no budgeting, only expenses WITH flag EKSPEDISI - ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, totalPopulation, totalWeightProduced) +func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem { + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) - items = append(items, HppItem{ - Type: "Beban Ekspedisi", + return HppItem{ + Type: HPPLabelEkspedisi, Comparison: ToComparison( ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), - ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), // Same as realization + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), ), - }) + } +} + +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup { + items := []HppItem{} + + budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + realizationAmount := getOperationalExpenses(realizations) + + if budgetAmount > 0 || realizationAmount > 0 { + items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx)) + } + + ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx)) return HppGroup{ - GroupName: "HPP dan Bahan Baku", + GroupName: HPPGroupBahanBaku, Data: items, } } // === HPP SUMMARY === -func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) SummaryHpp { - // Budget: purchases + budgets +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp { purchaseTotal := sumPurchaseTotal(purchaseItems) budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) totalBudget := purchaseTotal + budgetTotal - // Realization: all expenses totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) - budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, totalPopulation, totalWeightProduced) - realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, totalPopulation, totalWeightProduced) + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) return SummaryHpp{ Label: label, @@ -206,16 +250,16 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ } } -func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalWeightProduced, totalPopulation float64) HppPurchasesSection { +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection { hppGroups := []HppGroup{ { - GroupName: "HPP dan Pengeluaran", - Data: buildHppItemsByPurchaseFlags(purchaseItems, totalWeightProduced, totalPopulation), + GroupName: HPPGroupPengeluaran, + Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx), }, - ToHppBahanBakuGroup(budgets, realizations, totalWeightProduced, totalPopulation), + ToHppBahanBakuGroup(budgets, realizations, ctx), } - summaryHpp := ToSummaryHpp("HPP", purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) + summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx) return HppPurchasesSection{ Hpp: hppGroups, @@ -239,6 +283,11 @@ func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { } } +func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced) + return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { for _, item := range items { totalAmount += item.Amount @@ -247,63 +296,51 @@ func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { return } -func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []PLItem { +func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold) + return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + +func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem { items := []PLItem{} - // Categorize deliveries by sales type based on Product flags categorized := categorizeDeliveriesBySalesType(deliveryProducts) if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { - // For LAYING: show both Penjualan Ayam Besar and Penjualan Telur (even if 0) - ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) - telurAmount := sumDeliveriesByCategory(categorized["Penjualan Telur"]) + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg]) - // Penjualan Ayam Besar - rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) - - // Penjualan Telur - rpPerBird, rpPerKg = calculatePerUnitMetrics(telurAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Telur", ToFinancialMetrics(rpPerBird, rpPerKg, telurAmount))) + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) + items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx)) } else { - // For GROWING: show only Penjualan Ayam Besar - ayamAmount := sumDeliveriesByCategory(categorized["Penjualan Ayam Besar"]) - rpPerBird, rpPerKg := calculatePerUnitMetrics(ayamAmount, totalPopulation, totalWeightSold) - items = append(items, ToPLItem("Penjualan Ayam Besar", ToFinancialMetrics(rpPerBird, rpPerKg, ayamAmount))) + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) } return items } -func ToPembelianItems(purchases []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { - // Calculate total cost using same logic as report penjualan: - // Total Cost = All Purchase Items + All BOP Expenses +func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { purchaseAmount := sumPurchaseTotal(purchases) - - // Get BOP expenses (all expenses except ekspedisi) - bopAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - + bopAmount := getOperationalExpenses(realizations) totalCost := purchaseAmount + bopAmount - rpPerBird, rpPerKg := calculatePerUnitMetrics(totalCost, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Pembelian Sapronak", ToFinancialMetrics(rpPerBird, rpPerKg, totalCost)), + createPLItemWithMetrics(PLItemTypeSapronak, totalCost, ctx), } } -func ToOverheadItems(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { - realizationAmount := sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(realizationAmount, totalPopulation, totalWeightProduced) +func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { + realizationAmount := getOperationalExpenses(realizations) return []PLItem{ - ToPLItem("Pengeluaran Overhead", ToFinancialMetrics(rpPerBird, rpPerKg, realizationAmount)), + createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx), } } -func ToEkspedisiItems(realizations []entities.ExpenseRealization, totalPopulation, totalWeightProduced float64) []PLItem { +func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) - rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, totalPopulation, totalWeightProduced) return []PLItem{ - ToPLItem("Beban Ekspedisi", ToFinancialMetrics(rpPerBird, rpPerKg, amount)), + createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx), } } @@ -323,18 +360,17 @@ func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiIt netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird return PLSummaryGroup{ - GrossProfit: ToPLSummaryItem("LABA RUGI BRUTTO", ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), - SubTotal: ToPLSummaryItem("SUB TOTAL", ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), - NetProfit: ToPLSummaryItem("LABA RUGI NETTO", ToFinancialMetrics(netProfitPerBird, 0, netProfit)), + 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) - // Get total overhead and ekspedisi as single items - totalOverhead := aggregatePLItems(overheadItems, "Pengeluaran Overhead") - totalEkspedisi := aggregatePLItems(ekspedisiItems, "Beban Ekspedisi") + totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead) + totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi) return ProfitLossData{ Penjualan: penjualanItems, @@ -363,28 +399,31 @@ func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSec } } -func ToClosingKeuanganReport(projectFlockCategory string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, deliveryProducts []entities.MarketingDeliveryProduct, chickins []entities.ProjectChickin, totalWeightProduced, totalDepletion float64) ReportResponse { +func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse { var totalPopulation float64 var totalWeightSold float64 - for _, chickin := range chickins { + for _, chickin := range input.Chickins { totalPopulation += chickin.UsageQty } - for _, delivery := range deliveryProducts { + for _, delivery := range input.DeliveryProducts { totalWeightSold += delivery.TotalWeight } - // Calculate actual population (chickin - depletion) for cost allocation - actualPopulation := totalPopulation - totalDepletion + ctx := CalculationContext{ + TotalPopulation: totalPopulation, + TotalWeightProduced: input.TotalWeightProduced, + TotalDepletion: input.TotalDepletion, + TotalWeightSold: totalWeightSold, + ActualPopulation: totalPopulation - input.TotalDepletion, + } - // Use totalWeightProduced for HPP calculation (not totalWeightSold) - hppSection := ToHppPurchasesSection(purchaseItems, budgets, realizations, totalWeightProduced, totalPopulation) - - penjualanItems := ToPenjualanItems(projectFlockCategory, deliveryProducts, totalPopulation, totalWeightSold) - pembelianItems := ToPembelianItems(purchaseItems, budgets, realizations, actualPopulation, totalWeightProduced) - overheadItems := ToOverheadItems(budgets, realizations, actualPopulation, totalWeightProduced) - ekspedisiItems := ToEkspedisiItems(realizations, actualPopulation, totalWeightProduced) + hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, 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) @@ -402,17 +441,21 @@ func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) ( return rpPerBird, rpPerKg } +func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool { + for _, flag := range flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false +} + func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { return func(item *entities.PurchaseItem) bool { if item.Product == nil || len(item.Product.Flags) == 0 { return false } - for _, flag := range item.Product.Flags { - if strings.ToUpper(flag.Name) == string(flagType) { - return true - } - } - return false + return hasProductFlag(item.Product.Flags, flagType) } } @@ -421,13 +464,7 @@ func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.Exp if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil { return false } - nonstock := realization.ExpenseNonstock.Nonstock - for _, flag := range nonstock.Flags { - if strings.ToUpper(flag.Name) == string(flagType) { - return true - } - } - return false + return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType) } } @@ -438,46 +475,38 @@ func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.Expense } } -func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { +func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 { amount := 0.0 - for i := range purchases { - if filter(&purchases[i]) { - amount += purchases[i].TotalPrice + 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 { - amount := 0.0 - for i := range purchases { - amount += purchases[i].TotalPrice - } - return amount + 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 { - amount := 0.0 - for i := range budgets { - if filter(&budgets[i]) { - amount += budgets[i].Price * budgets[i].Qty - } - } - return amount + 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 { - amount := 0.0 - for i := range realizations { - if filter(&realizations[i]) { - amount += realizations[i].Price * realizations[i].Qty - } - } - return amount + 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 { @@ -500,21 +529,21 @@ func isEggProductFlag(flagType utils.FlagType) bool { func getSalesTypeFromProductFlags(product *entities.Product) string { if product == nil || len(product.Flags) == 0 { - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } for _, flag := range product.Flags { flagType := utils.FlagType(strings.ToUpper(flag.Name)) if isEggProductFlag(flagType) { - return "Penjualan Telur" + return PLSalesTypeEgg } if isChickenProductFlag(flagType) { - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } } - return "Penjualan Ayam Besar" + return PLSalesTypeChicken } func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index ea0ddb81..8c904561 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -35,8 +35,7 @@ type PenjualanRealisasiResponseDTO struct { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { - // todo: usia ayam masih dummy - age := 0 + age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -101,3 +100,20 @@ func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int } return 0 } + +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { + if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0 + } + + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) + ageInWeeks := ageInDays / 7 + return ageInWeeks +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 84b14ace..acb75871 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -137,6 +137,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit Preload("MarketingProduct.ProductWarehouse.Warehouse"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins"). Preload("MarketingProduct.Marketing"). Preload("MarketingProduct.Marketing.Customer"). Order("marketing_delivery_products.delivery_date DESC") @@ -450,13 +451,23 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (* s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) } - // Fetch depletion data to calculate actual population for cost allocation totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if err != nil { s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) } - report := dto.ToClosingKeuanganReport(projectFlock.Category, purchaseItems, budgets, realizations, deliveryProducts, chickins, totalWeightProduced, totalDepletion) + input := dto.ClosingKeuanganInput{ + ProjectFlockCategory: projectFlock.Category, + PurchaseItems: purchaseItems, + Budgets: budgets, + Realizations: realizations, + DeliveryProducts: deliveryProducts, + Chickins: chickins, + TotalWeightProduced: totalWeightProduced, + TotalDepletion: totalDepletion, + } + + report := dto.ToClosingKeuanganReport(input) return &report, nil } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index cb816431..b8eefa49 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -143,6 +143,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) } + if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) + } + chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) @@ -450,7 +454,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -466,7 +471,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -538,11 +544,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { +func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId) if err == nil && len(products) > 0 { - return &products[0], nil + existingPW := &products[0] + // Update project_flock_kandang_id if not already set + if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil { + existingPW.ProjectFlockKandangId = projectFlockKandangId + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil { + return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err) + } + } + return existingPW, nil } product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) @@ -554,9 +568,10 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId } newPW := &entity.ProductWarehouse{ - ProductId: product.Id, - WarehouseId: warehouseId, - Quantity: 0, + ProductId: product.Id, + WarehouseId: warehouseId, + ProjectFlockKandangId: projectFlockKandangId, + Quantity: 0, // CreatedBy: actorID, } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 7effdc35..cf2d87ee 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -190,13 +190,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project result := make(map[uint]float64) for _, pw := range products { - availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) - if err != nil { - s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) - } - if availableQty > 0 { - result[pw.Id] = availableQty + if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id { + availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) + } + + if availableQty > 0 { + result[pw.Id] = availableQty + } } } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 85c9a7fe..6e362ba7 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -425,7 +425,7 @@ func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context Joins("JOIN recordings ON recordings.id = recording_bws.recording_id"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings WHERE project_flock_kandangs_id = project_flock_kandangs.id)"). + Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID). Scan(&result).Error return result, err } diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 2f9b2774..fc599877 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -26,6 +26,7 @@ type PurchaseRepository interface { NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) + GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } type PurchaseRepositoryImpl struct { @@ -291,13 +292,34 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, } func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + + return r.GetItemsByWarehouseKandang(ctx, projectFlockID) +} + +func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { var items []entity.PurchaseItem + + var kandangIDs []uint err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Pluck("kandang_id", &kandangIDs).Error + + if err != nil { + return nil, err + } + + if len(kandangIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + err = r.DB().WithContext(ctx). Preload("Product"). Preload("Product.Flags"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = purchase_items.project_flock_kandang_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.kandang_id IN ?", kandangIDs). Find(&items).Error + return items, err } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 7513cbb1..ee00d0d8 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -123,25 +123,42 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { totalCost := s.getTotalProjectCost(ctx, projectFlockID) if totalCost == 0 { + s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID) return 0 } - chickinQty, _ := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) - depletion, _ := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) - avgWeight, _ := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err) + } + + depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err) + } + + avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err) + } var totalWeight float64 if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { totalWeight = (chickinQty - depletion) * avgWeight } else { - eggWeight, _ := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err) + } totalWeight = (chickinQty-depletion)*avgWeight + eggWeight } if totalWeight == 0 { return 0 } - return totalCost / totalWeight + + hppPricePerKg := totalCost / totalWeight + return hppPricePerKg } func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { @@ -151,24 +168,30 @@ func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { - s.Log.Warnf("GetItemsByProjectFlockID error: %v", err) + s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) + return 0 } cost := float64(0) + purchaseCost := float64(0) for _, p := range purchases { - cost += p.TotalPrice + purchaseCost += p.TotalPrice } + cost += purchaseCost realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) if err != nil { - s.Log.Warnf("GetByProjectFlockID error: %v", err) + s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) } + bopCost := float64(0) for _, r := range realizations { if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { - cost += r.Price * r.Qty + bopCost += r.Price * r.Qty } } + cost += bopCost + return cost } From ab9c7c216aad8f7d3e2bbf1d41a1a52dc6d0b28e Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 19 Dec 2025 14:37:54 +0700 Subject: [PATCH 24/31] Feat(BE-304): add permission in report and closing --- internal/capabilities/capabilities.go | 44 ------ internal/middleware/permissions.go | 220 ++++++++++++++------------ internal/modules/closings/route.go | 11 +- internal/modules/repports/module.go | 7 +- internal/modules/repports/route.go | 9 +- 5 files changed, 133 insertions(+), 158 deletions(-) delete mode 100644 internal/capabilities/capabilities.go diff --git a/internal/capabilities/capabilities.go b/internal/capabilities/capabilities.go deleted file mode 100644 index 47f774ba..00000000 --- a/internal/capabilities/capabilities.go +++ /dev/null @@ -1,44 +0,0 @@ -package capabilities - -import ( - "strings" - - permission "gitlab.com/mbugroup/lti-api.git/internal/middleware" -) - -// FromPermissions returns a filtered map of capabilities that the frontend can use -// to toggle features. Only permissions recognized by the application are exposed. -func FromPermissions(perms []string) map[string]bool { - if len(perms) == 0 { - return nil - } - - out := make(map[string]bool) - for _, perm := range perms { - if key, ok := normalizeAndAllow(perm); ok { - out[key] = true - } - } - if len(out) == 0 { - return nil - } - return out -} - -func normalizeAndAllow(perm string) (string, bool) { - perm = strings.ToLower(strings.TrimSpace(perm)) - if perm == "" { - return "", false - } - if _, ok := allowed[perm]; !ok { - return "", false - } - return perm, true -} - -var allowed = map[string]struct{}{ - permission.PermissionRecordingRead: {}, - permission.PermissionRecordingCreate: {}, - permission.PermissionRecordingUpdate: {}, - permission.PermissionRecordingDelete: {}, -} diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 0734b035..462bc8b7 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -1,183 +1,197 @@ package middleware -//project-flock +// project-flock const ( P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" - P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" - P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" + P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" + P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" - P_ProjectFlockGetAll = "lti.production.project_flocks.list" - P_ProjectFlockCreate = "lti.production.project_flocks.create" - P_ProjectFlockGetOne = "lti.production.project_flocks.detail" - P_ProjectFlockUpdate = "lti.production.project_flocks.update" - P_ProjectFlockDelete = "lti.production.project_flocks.delete" - P_ProjectFlockApprove = "lti.production.project_flocks.approve" - P_ProjectFlockLookup = "lti.production.project_flocks.lookup" + P_ProjectFlockGetAll = "lti.production.project_flocks.list" + P_ProjectFlockCreate = "lti.production.project_flocks.create" + P_ProjectFlockGetOne = "lti.production.project_flocks.detail" + P_ProjectFlockUpdate = "lti.production.project_flocks.update" + P_ProjectFlockDelete = "lti.production.project_flocks.delete" + P_ProjectFlockApprove = "lti.production.project_flocks.approve" + P_ProjectFlockLookup = "lti.production.project_flocks.lookup" P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period" - P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit" + P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit" ) -const( - P_ExpenseGetAll= "lti.expense.list" - P_ExpenseCreateOne= "lti.expense.create" - P_ExpenseUpdateOne= "lti.expense.update" - P_ExpenseGetOne= "lti.expense.detail" - P_ExpenseDeleteOne= "lti.expense.delete" - P_ExpenseApprovalManager= "lti.expense.approve.manager" - P_ExpenseApprovalFinance= "lti.expense.approve.finance" - P_ExpenseCreateRealizations= "lti.expense.create.realization" - P_ExpenseUpdateRealizations= "lti.expense.update.realization" - P_ExpenseCompleteExpense= "lti.expense.complete.expense" - P_ExpenseDocument= "lti.expense.document" - P_ExpenseDocumentRealizations= "lti.expense.document.realization" +const ( + P_ExpenseGetAll = "lti.expense.list" + P_ExpenseCreateOne = "lti.expense.create" + P_ExpenseUpdateOne = "lti.expense.update" + P_ExpenseGetOne = "lti.expense.detail" + P_ExpenseDeleteOne = "lti.expense.delete" + P_ExpenseApprovalManager = "lti.expense.approve.manager" + P_ExpenseApprovalFinance = "lti.expense.approve.finance" + P_ExpenseCreateRealizations = "lti.expense.create.realization" + P_ExpenseUpdateRealizations = "lti.expense.update.realization" + P_ExpenseCompleteExpense = "lti.expense.complete.expense" + P_ExpenseDocument = "lti.expense.document" + P_ExpenseDocumentRealizations = "lti.expense.document.realization" ) -const( - P_AdjustmentGetAll="lti.inventory.list" - P_AdjustmentCreate="lti.inventory.create" - P_AdjustmentGetOne="lti.inventory.detail" +const ( + P_AdjustmentGetAll = "lti.inventory.list" + P_AdjustmentCreate = "lti.inventory.create" + P_AdjustmentGetOne = "lti.inventory.detail" ) -const( +const ( P_ApprovalGetAll = "lti.approval.list" ) - -const( - P_ClosingGetAll = "lti.closing.list" - P_ClosingPenjualan = "lti.closing.penjualan" - P_ClosingGetSummary = "lti.closing.getsummary" - P_ProductStockGetAll = "lti.inventory.product_stock.list" - P_ProductStockGetOne = "lti.inventory.product_stock.detail" - P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list" - P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" +const ( + P_ReportExpenseGetAll = "lti.repport.expense.list" + P_ReportDeliveryGetAll = "lti.repport.delivery.list" ) -const( - P_TransferGetAll = "lti.inventory.transfer.list" - P_TransferGetOne = "lti.inventory.transfer.detail" + +const ( + P_ProductStockGetAll = "lti.inventory.product_stock.list" + P_ProductStockGetOne = "lti.inventory.product_stock.detail" + P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list" + P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" +) +const ( + P_ClosingGetAll = "lti.closing.list" + P_ClosingPenjualan = "lti.closing.penjualan" + P_ClosingGetSummary = "lti.closing.getsummary" + + + //?baru + P_ClosingGetOverhead = "lti.closing.getoverhead" + P_ClosingCountSapronakKandang = "lti.closing.getsapronakcountbykandang" + P_ClosingCountSapronak = "lti.closing.getsapronakcount" + P_ClosingSapronak = "lti.closing.getsapronak" + +) + +const ( + P_TransferGetAll = "lti.inventory.transfer.list" + P_TransferGetOne = "lti.inventory.transfer.detail" P_TransferCreateOne = "lti.inventory.transfer.create" ) -const( - P_DeliveryGetAll = "lti.marketing.delivery_order.list" - P_DeliveryGetOne = "lti.marketing.delivery_order.detail" - P_DeliveryCreateOne = "lti.marketing.delivery_order.create" - P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" - P_SalesOrderDelete = "lti.marketing.sales_order.delete" - P_SalesOrderApproval = "lti.marketing.sales_order.approve" +const ( + P_DeliveryGetAll = "lti.marketing.delivery_order.list" + P_DeliveryGetOne = "lti.marketing.delivery_order.detail" + P_DeliveryCreateOne = "lti.marketing.delivery_order.create" + P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" + P_SalesOrderDelete = "lti.marketing.sales_order.delete" + P_SalesOrderApproval = "lti.marketing.sales_order.approve" P_SalesOrderCreateOne = "lti.marketing.sales_order.create" P_SalesOrderUpdateOne = "lti.marketing.sales_order.update" ) -const( - P_AreaGetAll = "lti.master.area.list" - P_AreaGetOne = "lti.master.area.detail" +const ( + P_AreaGetAll = "lti.master.area.list" + P_AreaGetOne = "lti.master.area.detail" P_AreaCreateOne = "lti.master.area.create" P_AreaUpdateOne = "lti.master.area.update" P_AreaDeleteOne = "lti.master.area.delete" - P_BanksGetAll = "lti.master.banks.list" - P_BanksGetOne = "lti.master.banks.detail" + P_BanksGetAll = "lti.master.banks.list" + P_BanksGetOne = "lti.master.banks.detail" P_BanksCreateOne = "lti.master.banks.create" P_BanksUpdateOne = "lti.master.banks.update" P_BanksDeleteOne = "lti.master.banks.delete" - P_CustomerGetAll = "lti.master.customer.list" - P_CustomerGetOne = "lti.master.customer.detail" + P_CustomerGetAll = "lti.master.customer.list" + P_CustomerGetOne = "lti.master.customer.detail" P_CustomerCreateOne = "lti.master.customer.create" P_CustomerUpdateOne = "lti.master.customer.update" P_CustomerDeleteOne = "lti.master.customer.delete" - - P_FcrGetAll = "lti.master.fcr.list" - P_FcrGetOne = "lti.master.fcr.detail" + + P_FcrGetAll = "lti.master.fcr.list" + P_FcrGetOne = "lti.master.fcr.detail" P_FcrCreateOne = "lti.master.fcr.create" P_FcrUpdateOne = "lti.master.fcr.update" P_FcrDeleteOne = "lti.master.fcr.delete" - - P_FlocksGetAll = "lti.master.flocks.list" - P_FlocksGetOne = "lti.master.flocks.detail" + + P_FlocksGetAll = "lti.master.flocks.list" + P_FlocksGetOne = "lti.master.flocks.detail" P_FlocksCreateOne = "lti.master.flocks.create" P_FlocksUpdateOne = "lti.master.flocks.update" P_FlocksDeleteOne = "lti.master.flocks.delete" - - P_KandangsGetAll = "lti.master.kandangs.list" - P_KandangsGetOne = "lti.master.kandangs.detail" + + P_KandangsGetAll = "lti.master.kandangs.list" + P_KandangsGetOne = "lti.master.kandangs.detail" P_KandangsCreateOne = "lti.master.kandangs.create" P_KandangsUpdateOne = "lti.master.kandangs.update" P_KandangsDeleteOne = "lti.master.kandangs.delete" - - P_LocationsGetAll = "lti.master.locations.list" - P_LocationsGetOne = "lti.master.locations.detail" + + P_LocationsGetAll = "lti.master.locations.list" + P_LocationsGetOne = "lti.master.locations.detail" P_LocationsCreateOne = "lti.master.locations.create" P_LocationsUpdateOne = "lti.master.locations.update" P_LocationsDeleteOne = "lti.master.locations.delete" - - P_NonstocksGetAll = "lti.master.nonstocks.list" - P_NonstocksGetOne = "lti.master.nonstocks.detail" + + P_NonstocksGetAll = "lti.master.nonstocks.list" + P_NonstocksGetOne = "lti.master.nonstocks.detail" P_NonstocksCreateOne = "lti.master.nonstocks.create" P_NonstocksUpdateOne = "lti.master.nonstocks.update" P_NonstocksDeleteOne = "lti.master.nonstocks.delete" - P_ProductCategoriesGetAll = "lti.master.Product_categories.list" - P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" + P_ProductCategoriesGetAll = "lti.master.Product_categories.list" + P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" - - P_ProductsGetAll = "lti.master.Products.list" - P_ProductsGetOne = "lti.master.Products.detail" + + P_ProductsGetAll = "lti.master.Products.list" + P_ProductsGetOne = "lti.master.Products.detail" P_ProductsCreateOne = "lti.master.Products.create" P_ProductsUpdateOne = "lti.master.Products.update" P_ProductsDeleteOne = "lti.master.Products.delete" - - P_SuppliersGetAll = "lti.master.suppliers.list" - P_SuppliersGetOne = "lti.master.suppliers.detail" + + P_SuppliersGetAll = "lti.master.suppliers.list" + P_SuppliersGetOne = "lti.master.suppliers.detail" P_SuppliersCreateOne = "lti.master.suppliers.create" P_SuppliersUpdateOne = "lti.master.suppliers.update" P_SuppliersDeleteOne = "lti.master.suppliers.delete" - P_UomsGetAll = "lti.master.uoms.list" - P_UomsGetOne = "lti.master.uoms.detail" + P_UomsGetAll = "lti.master.uoms.list" + P_UomsGetOne = "lti.master.uoms.detail" P_UomsCreateOne = "lti.master.uoms.create" P_UomsUpdateOne = "lti.master.uoms.update" P_UomsDeleteOne = "lti.master.uoms.delete" - P_WarehousesGetAll = "lti.master.warehouses.list" - P_WarehousesGetOne = "lti.master.warehouses.detail" + P_WarehousesGetAll = "lti.master.warehouses.list" + P_WarehousesGetOne = "lti.master.warehouses.detail" P_WarehousesCreateOne = "lti.master.warehouses.create" P_WarehousesUpdateOne = "lti.master.warehouses.update" P_WarehousesDeleteOne = "lti.master.warehouses.delete" - ) - -const( +const ( P_ChickinsCreateOne = "lti.production.chickins.create" - P_ChickinsGetOne = "lti.production.chickins.detail" - P_ChickinsApproval = "lti.production.chickins.approve" + P_ChickinsGetOne = "lti.production.chickins.detail" + P_ChickinsApproval = "lti.production.chickins.approve" ) -//recording + +// recording const ( - P_RecordingGetAll = "lti.production.recording.list" - P_RecordingGetOne = "lti.production.recording.detail" - P_RecordingCreateOne = "lti.production.recording.create" - P_RecordingUpdateOne = "lti.production.recording.update" - P_RecordingDeleteOne = "lti.production.recording.delete" + P_RecordingGetAll = "lti.production.recording.list" + P_RecordingGetOne = "lti.production.recording.detail" + P_RecordingCreateOne = "lti.production.recording.create" + P_RecordingUpdateOne = "lti.production.recording.update" + P_RecordingDeleteOne = "lti.production.recording.delete" P_RecordingNextDay = "lti.production.recording.next_day" - P_RecordingApproval = "lti.production.recording.approve" + P_RecordingApproval = "lti.production.recording.approve" ) const ( - P_PurchaseGetAll = "lti.Purchase.list" - P_PurchaseGetOne = "lti.Purchase.detail" - P_PurchaseCreateOne = "lti.Purchase.create" - P_PurchaseUpdateOne = "lti.Purchase.update" - P_PurchaseDeleteOne = "lti.Purchase.delete" + P_PurchaseGetAll = "lti.Purchase.list" + P_PurchaseGetOne = "lti.Purchase.detail" + P_PurchaseCreateOne = "lti.Purchase.create" + P_PurchaseUpdateOne = "lti.Purchase.update" + P_PurchaseDeleteOne = "lti.Purchase.delete" P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" - P_PurchaseReceive = "lti.Purchase.receive" + P_PurchaseReceive = "lti.Purchase.receive" P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" - P_PurchaseApprovalManager = "lti.Purchase.approve.manager" + P_PurchaseApprovalManager = "lti.Purchase.approve.manager" ) -const( +const ( P_UserGetAll = "lti.users.list" P_UserGetOne = "lti.users.detail" -) \ No newline at end of file +) diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 5033f989..38f8a816 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -24,11 +24,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/",m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) route.Get("/:project_flock_id/penjualan",m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan) route.Get("/:projectFlockId",m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary) - route.Get("/", ctrl.GetAll) - route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan) - route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) - route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang) - route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject) - route.Get("/:projectFlockId", ctrl.GetClosingSummary) - route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + route.Get("/:project_flock_id/overhead",m.RequirePermissions(m.P_ClosingGetOverhead), ctrl.GetOverhead) + route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak",m.RequirePermissions(m.P_ClosingCountSapronakKandang) ,ctrl.GetSapronakByKandang) + route.Get("/:project_flock_id/perhitungan_sapronak",m.RequirePermissions(m.P_ClosingCountSapronak) ,ctrl.GetSapronakByProject) + route.Get("/:projectFlockId/sapronak",m.RequirePermissions(m.P_ClosingSapronak), ctrl.GetClosingSapronak) } diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..c1a00e8c 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -11,6 +11,9 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) type RepportModule struct{} @@ -20,9 +23,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) + userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + userService := sUser.NewUserService(userRepository, validate) - RepportRoutes(router, repportService) + RepportRoutes(router, userService, repportService) } diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 4aea831c..4edba9c7 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -1,17 +1,20 @@ package repports import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers" repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "github.com/gofiber/fiber/v2" ) -func RepportRoutes(v1 fiber.Router, s repport.RepportService) { +func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService) { ctrl := controller.NewRepportController(s) route := v1.Group("/reports") + route.Use(m.Auth(u)) - route.Get("/expense", ctrl.GetExpense) - route.Get("/marketing", ctrl.GetMarketing) + route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) + route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) } From 1af8f0a72600430e6f80932bc84e6ddd4a2e4348 Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 19 Dec 2025 15:55:30 +0700 Subject: [PATCH 25/31] Feat(BE-304): add permission in report and closing --- internal/middleware/permissions.go | 10 ++++++---- internal/modules/closings/route.go | 20 ++++++++++---------- internal/modules/repports/route.go | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 462bc8b7..e715aae9 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -42,6 +42,7 @@ const ( const ( P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list" + P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" ) @@ -55,14 +56,15 @@ const ( P_ClosingGetAll = "lti.closing.list" P_ClosingPenjualan = "lti.closing.penjualan" P_ClosingGetSummary = "lti.closing.getsummary" - - - //?baru P_ClosingGetOverhead = "lti.closing.getoverhead" - P_ClosingCountSapronakKandang = "lti.closing.getsapronakcountbykandang" + P_ClosingCountSapronakKandang = "lti.closing.getsapronakcount.kandang" P_ClosingCountSapronak = "lti.closing.getsapronakcount" P_ClosingSapronak = "lti.closing.getsapronak" + P_ClosingExpeditionHpp = "lti.closing.expedition" + P_ClosingExpeditionHppByKandang = "lti.closing.expedition.kandang" + P_ClosingDataProduction = "lti.closing.production.data" + ) const ( diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 58372183..d4250624 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -21,14 +21,14 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/",m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) - route.Get("/:project_flock_id/penjualan",m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan) - route.Get("/:projectFlockId",m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary) - route.Get("/:project_flock_id/overhead",m.RequirePermissions(m.P_ClosingGetOverhead), ctrl.GetOverhead) - route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak",m.RequirePermissions(m.P_ClosingCountSapronakKandang) ,ctrl.GetSapronakByKandang) - route.Get("/:project_flock_id/perhitungan_sapronak",m.RequirePermissions(m.P_ClosingCountSapronak) ,ctrl.GetSapronakByProject) - route.Get("/:projectFlockId/sapronak",m.RequirePermissions(m.P_ClosingSapronak), ctrl.GetClosingSapronak) - route.Get("/:project_flock_id/expedition-hpp", ctrl.GetExpeditionHPP) - route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", ctrl.GetExpeditionHPPByKandang) - route.Get("/:projectFlockId/data-produksi", ctrl.GetClosingDataProduksi) + route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) + route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan) + route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary) + route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingGetOverhead), ctrl.GetOverhead) + route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingCountSapronakKandang), ctrl.GetSapronakByKandang) + route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingCountSapronak), ctrl.GetSapronakByProject) + route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingSapronak), ctrl.GetClosingSapronak) + route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHpp), ctrl.GetExpeditionHPP) + route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHppByKandang), ctrl.GetExpeditionHPPByKandang) + route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi) } diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 93758f07..45dc32b7 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -17,5 +17,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) - route.Get("/purchase-supplier", ctrl.GetPurchaseSupplier) + route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) } From dc726c49cf04b48efefc16775d9fe9367e499155 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Sun, 21 Dec 2025 13:03:32 +0700 Subject: [PATCH 26/31] adjust age closing data produksi --- internal/modules/closings/dto/closing.dto.go | 17 +++++++ .../closings/services/closing.service.go | 45 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 429495b7..c05bd741 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -204,3 +204,20 @@ func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO { ClosingListDTO: ToClosingListDTO(e), } } + +func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { + if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0 + } + + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) + ageInWeeks := ageInDays / 7 + return ageInWeeks +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 895f05f7..10007fd9 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -469,8 +469,11 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") } } - // masih dummy, karena tab penjualan agenya masih dummy - age := 1.0 + age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") + } feedUsedPerHead := 0.0 if population > 0 { @@ -599,6 +602,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return &result, nil } +func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) { + deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB { + return db. + Preload("MarketingProduct"). + Preload("MarketingProduct.ProductWarehouse"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins") + }) + if err != nil { + return 0, err + } + + var ( + totalQty float64 + totalAgeWeeks float64 + ) + + for _, product := range deliveryProducts { + if product.Qty == 0 { + continue + } + projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang + ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate) + totalAgeWeeks += float64(ageWeeks) * product.Qty + totalQty += product.Qty + } + + if totalQty == 0 { + return 0, nil + } + + return totalAgeWeeks / totalQty, nil +} + func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) @@ -612,8 +649,8 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul mortalityAct = (depletion / basePopulation) * 100 } - deffMortality := mortalityStd - mortalityAct - deffFcr := fcrStd - fcrAct + deffMortality := mortalityAct - mortalityStd + deffFcr := fcrAct - fcrStd awg := 0.0 if age > 0 { From ef117e66d16b0f87214a274691658cb71061c6e0 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 22 Dec 2025 10:03:32 +0700 Subject: [PATCH 27/31] add permission deliveryorder and sales order --- internal/middleware/permissions.go | 11 ++++++++++- internal/modules/marketing/route.go | 16 ++++++---------- .../modules/production/transfer_layings/route.go | 14 +++++++------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index e715aae9..9e2b5e5e 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -73,10 +73,19 @@ const ( P_TransferCreateOne = "lti.inventory.transfer.create" ) +const ( + P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list" + P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail" + P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create" + P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update" + P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete" + P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve" + P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty" +) + const ( P_DeliveryGetAll = "lti.marketing.delivery_order.list" P_DeliveryGetOne = "lti.marketing.delivery_order.detail" - P_DeliveryCreateOne = "lti.marketing.delivery_order.create" P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" P_SalesOrderDelete = "lti.marketing.sales_order.delete" P_SalesOrderApproval = "lti.marketing.sales_order.approve" diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 75ecc0f6..139d1ee9 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -16,16 +16,12 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde route := router.Group("/marketing") route.Use(m.Auth(userService)) - route.Get("/", deliveryOrdersCtrl.GetAll) - route.Get("/:id", deliveryOrdersCtrl.GetOne) - route.Delete("/:id", salesOrdersCtrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) + route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) - route.Post("/sales-orders", salesOrdersCtrl.CreateOne) - route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne) - route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval) + route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) + route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne) + route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval) - route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll) - route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne) - route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne) - route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne) } diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 868454c5..8f7a62c0 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -21,11 +21,11 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Post("/approval", m.Auth(u), ctrl.Approval) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) - route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang) + route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) + route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) } From 817b6f82d02dace70972e9787b8b90e274c4513f Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Mon, 22 Dec 2025 15:15:42 +0700 Subject: [PATCH 28/31] rename api closing data produksi --- internal/modules/closings/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index d4250624..26235b7f 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -30,5 +30,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingSapronak), ctrl.GetClosingSapronak) route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHpp), ctrl.GetExpeditionHPP) route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHppByKandang), ctrl.GetExpeditionHPPByKandang) - route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi) + route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi) } From 2d8f20b70ed26de2d05536cf0039a9f3cc4ebf7e Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 23 Dec 2025 08:57:41 +0700 Subject: [PATCH 29/31] Fix(BE-304):add refresh token and adjustment permission --- internal/middleware/permissions.go | 35 +++----- internal/modules/closings/route.go | 20 ++--- .../project-flock-kandangs/route.go | 4 +- .../sso/controllers/refresh_token_response.go | 13 +++ .../modules/sso/controllers/sso.controller.go | 80 +++++++++++++++++++ internal/modules/sso/route.go | 1 + 6 files changed, 119 insertions(+), 34 deletions(-) create mode 100644 internal/modules/sso/controllers/refresh_token_response.go diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index f9f3ec6e..f46c25a9 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -2,9 +2,10 @@ package middleware // project-flock const ( - P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" - P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" - P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" + P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" + P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail" + P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" + P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" P_ProjectFlockGetAll = "lti.production.project_flocks.list" P_ProjectFlockCreate = "lti.production.project_flocks.create" @@ -52,18 +53,8 @@ const ( P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" ) const ( - P_ClosingGetAll = "lti.closing.list" - P_ClosingPenjualan = "lti.closing.penjualan" - P_ClosingGetSummary = "lti.closing.getsummary" - P_ClosingGetOverhead = "lti.closing.getoverhead" - P_ClosingCountSapronakKandang = "lti.closing.getsapronakcount.kandang" - P_ClosingCountSapronak = "lti.closing.getsapronakcount" - P_ClosingSapronak = "lti.closing.getsapronak" - - P_ClosingExpeditionHpp = "lti.closing.expedition" - P_ClosingExpeditionHppByKandang = "lti.closing.expedition.kandang" - P_ClosingDataProduction = "lti.closing.production.data" - P_ClosingKeuangan = "lti.closing.keuangan" + P_ClosingGetAll = "lti.closing.list" + P_ClosingDetail = "lti.closing.detail" ) const ( @@ -73,13 +64,13 @@ const ( ) const ( - P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list" - P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail" - P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create" - P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update" - P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete" - P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve" - P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty" + P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list" + P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail" + P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create" + P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update" + P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete" + P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve" + P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty" ) const ( diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index 7f517c10..52333b67 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -22,14 +22,14 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) - route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan) - route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary) - route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingGetOverhead), ctrl.GetOverhead) - route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingCountSapronakKandang), ctrl.GetSapronakByKandang) - route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingCountSapronak), ctrl.GetSapronakByProject) - route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingSapronak), ctrl.GetClosingSapronak) - route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHpp), ctrl.GetExpeditionHPP) - route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingExpeditionHppByKandang), ctrl.GetExpeditionHPPByKandang) - route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDataProduction), ctrl.GetClosingDataProduksi) - route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingKeuangan), ctrl.GetClosingKeuangan) + route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan) + route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary) + route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead) + route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) + route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) + route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) + route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) + route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang) + route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi) + route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan) } diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index c5dba313..d48d9990 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -18,6 +18,6 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) // route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing) // route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing) - route.Post("/:id/closing", ctrl.Closing) - route.Get("/:id/closing/check", ctrl.CheckClosing) + route.Post("/:id/closing",m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing) + route.Get("/:id/closing/check", m.RequirePermissions(m.P_ProjectFlockKandangsCheckClosing), ctrl.CheckClosing) } diff --git a/internal/modules/sso/controllers/refresh_token_response.go b/internal/modules/sso/controllers/refresh_token_response.go new file mode 100644 index 00000000..1825342a --- /dev/null +++ b/internal/modules/sso/controllers/refresh_token_response.go @@ -0,0 +1,13 @@ +package controllers + +type refreshTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + IDToken string `json:"id_token"` + Error string `json:"error"` + Description string `json:"error_description"` +} + diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index f11a31c8..99bd67d6 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -138,6 +138,86 @@ func (h *Controller) Start(c *fiber.Ctx) error { return c.Redirect(authorizeURL.String(), fiber.StatusFound) } +// Refresh exchanges the current SSO refresh token for a new access/refresh pair +// without redirecting the browser to the SSO login page. +func (h *Controller) Refresh(c *fiber.Ctx) error { + refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh") + refreshToken := strings.TrimSpace(c.Cookies(refreshName)) + if refreshToken == "" { + return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated") + } + + tokenEndpoint := strings.TrimSpace(config.SSOTokenURL) + if tokenEndpoint == "" { + return fiber.NewError(fiber.StatusInternalServerError, "token endpoint not configured") + } + + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", refreshToken) + + req, err := http.NewRequestWithContext(c.Context(), http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode())) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to create refresh request") + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := h.httpClient.Do(req) + if err != nil { + utils.Log.Errorf("token refresh request failed: %v", err) + return fiber.NewError(fiber.StatusBadGateway, "failed to refresh access token") + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + utils.Log.Warnf("token refresh response status %d", resp.StatusCode) + return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated") + } + + var tokenResp refreshTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return fiber.NewError(fiber.StatusBadGateway, "invalid token response") + } + if tokenResp.Error != "" { + return fiber.NewError(fiber.StatusBadGateway, tokenResp.Description) + } + if tokenResp.AccessToken == "" { + return fiber.NewError(fiber.StatusBadGateway, "missing access token") + } + + verification, err := sso.VerifyAccessToken(tokenResp.AccessToken) + if err != nil { + utils.Log.Errorf("access token verification failed: %v", err) + return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") + } + + issueCookies(c, struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + IDToken string `json:"id_token"` + Error string `json:"error"` + Description string `json:"error_description"` + }{ + AccessToken: tokenResp.AccessToken, + RefreshToken: tokenResp.RefreshToken, + TokenType: tokenResp.TokenType, + ExpiresIn: tokenResp.ExpiresIn, + Scope: tokenResp.Scope, + IDToken: tokenResp.IDToken, + Error: tokenResp.Error, + Description: tokenResp.Description, + }, verification) + + utils.Log.WithFields(logrus.Fields{ + "user_id": verification.UserID, + }).Info("sso refresh successful") + + return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"}) +} + // Callback handles the redirect from SSO containing the authorization code. func (h *Controller) Callback(c *fiber.Ctx) error { state := strings.TrimSpace(c.Query("state")) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index a7288ef9..3f2a699e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -31,6 +31,7 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) group.Get("/callback", ctrl.Callback) group.Get("/userinfo", middleware.NewLimiter(60, time.Minute), ctrl.UserInfo) + group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) } From a2b8ebe6652a1bc13e24bc52bc12f951a93296bb Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 23 Dec 2025 11:50:00 +0700 Subject: [PATCH 30/31] Fix(BE-278):fixing total price in purchase --- internal/middleware/auth.go | 115 +++++++++--------- internal/modules/purchases/route.go | 12 +- .../purchases/services/purchase.service.go | 19 ++- 3 files changed, 81 insertions(+), 65 deletions(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index a831c25b..85bb8146 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/gofiber/fiber/v2" - "gitlab.com/mbugroup/lti-api.git/internal/config" + // "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -31,65 +31,65 @@ type AuthContext struct { // fine-grained authorization using the SSO access token scopes. func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { return func(c *fiber.Ctx) error { - token := bearerToken(c) - if token == "" { - token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) - } - if token == "" { - return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } + // token := bearerToken(c) + // if token == "" { + // token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) + // } + // if token == "" { + // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } - verification, err := sso.VerifyAccessToken(token) - if err != nil { - utils.Log.WithError(err).Warn("auth: token verification failed") - return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } + // verification, err := sso.VerifyAccessToken(token) + // if err != nil { + // utils.Log.WithError(err).Warn("auth: token verification failed") + // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } - if verification.UserID == 0 { - return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") - } + // if verification.UserID == 0 { + // return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") + // } - if err := ensureNotRevoked(c, token, verification); err != nil { - return err - } + // if err := ensureNotRevoked(c, token, verification); err != nil { + // return err + // } - user, err := userService.GetBySSOUserID(c, verification.UserID) - if err != nil || user == nil { - utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") - return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } + // user, err := userService.GetBySSOUserID(c, verification.UserID) + // if err != nil || user == nil { + // utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") + // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } - if len(requiredScopes) > 0 { - if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { - return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") - } - } + // if len(requiredScopes) > 0 { + // if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { + // return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") + // } + // } - var roles []sso.Role - permissions := make(map[string]struct{}) - if verification.UserID != 0 { - if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { - utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") - } else if profile != nil { - roles = profile.Roles - for _, perm := range profile.PermissionNames() { - if perm != "" { - permissions[perm] = struct{}{} - } - } - } - } + // var roles []sso.Role + // permissions := make(map[string]struct{}) + // if verification.UserID != 0 { + // if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { + // utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") + // } else if profile != nil { + // roles = profile.Roles + // for _, perm := range profile.PermissionNames() { + // if perm != "" { + // permissions[perm] = struct{}{} + // } + // } + // } + // } - ctx := &AuthContext{ - Token: token, - Verification: verification, - User: user, - Roles: roles, - Permissions: permissions, - } + // ctx := &AuthContext{ + // Token: token, + // Verification: verification, + // User: user, + // Roles: roles, + // Permissions: permissions, + // } - c.Locals(authContextLocalsKey, ctx) - c.Locals(authUserLocalsKey, user) + // c.Locals(authContextLocalsKey, ctx) + // c.Locals(authUserLocalsKey, user) return c.Next() } } @@ -104,11 +104,12 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) { } func ActorIDFromContext(c *fiber.Ctx) (uint, error) { - user, ok := AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } - return user.Id, nil + // user, ok := AuthenticatedUser(c) + // if !ok || user == nil || user.Id == 0 { + // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } + // return user.Id, nil + return 1, nil } // AuthDetails returns the full authentication context (token, claims, user). diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index 4be485e6..0fe038c3 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -17,10 +17,10 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe route.Get("/",m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll) route.Get("/:id",m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne) - route.Post("/",m.RequirePermissions(m.P_PurchaseCreateOne), ctrl.CreateOne) - route.Post("/:id/approvals/staff",m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase) - route.Post("/:id/approvals/manager",m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase) - route.Post("/:id/receipts",m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts) - route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeletePurchase) - route.Delete("/:id/items",m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems) + route.Post("/", ctrl.CreateOne) + route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) + route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase) + route.Post("/:id/receipts",ctrl.ReceiveProducts) + route.Delete("/:id", ctrl.DeletePurchase) + route.Delete("/:id/items", ctrl.DeleteItems) } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 64a91e9d..366a8c0e 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -247,7 +247,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, nil, utils.Internal("Failed to get warehouse") } if warehouse.KandangId == nil || *warehouse.KandangId == 0 { - return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not linked to a kandang", id)) + return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name)) } var pfkID *uint if s.ProjectFlockKandangRepo != nil { @@ -258,7 +258,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase idCopy := uint(pfk.Id) pfkID = &idCopy } else if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d has no active project flock", id)) + return nil, nil, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name)) } else if err != nil { s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err) return nil, nil, utils.Internal("Failed to validate project flock") @@ -794,6 +794,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation deltas := make(map[uint]float64) affected := make(map[uint]struct{}) updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared)) + priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared)) fifoAdds := make([]struct { itemID uint pwID uint @@ -862,6 +863,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } updates = append(updates, update) + + if item.Price > 0 && prep.receivedQty >= 0 { + priceUpdates = append(priceUpdates, rPurchase.PurchasePricingUpdate{ + ItemID: item.Id, + Price: item.Price, + TotalPrice: item.Price * prep.receivedQty, + }) + } } if err := repoTx.UpdateReceivingDetails(c.Context(), purchase.Id, updates); err != nil { @@ -876,6 +885,12 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return err } + if len(priceUpdates) > 0 { + if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil { + return err + } + } + // Update due_date based on earliest received date when receiving approved. if earliestReceived != nil { due := earliestReceived.AddDate(0, 0, purchase.CreditTerm) From b41bb7912575fe83e72b21e74574f18b1e2caf08 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 23 Dec 2025 11:50:45 +0700 Subject: [PATCH 31/31] Fix(BE-304):uncomment auth --- internal/middleware/auth.go | 104 ++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 85bb8146..a08d431b 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -4,7 +4,7 @@ import ( "strings" "github.com/gofiber/fiber/v2" - // "gitlab.com/mbugroup/lti-api.git/internal/config" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -31,65 +31,65 @@ type AuthContext struct { // fine-grained authorization using the SSO access token scopes. func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { return func(c *fiber.Ctx) error { - // token := bearerToken(c) - // if token == "" { - // token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) - // } - // if token == "" { - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + token := bearerToken(c) + if token == "" { + token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) + } + if token == "" { + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // verification, err := sso.VerifyAccessToken(token) - // if err != nil { - // utils.Log.WithError(err).Warn("auth: token verification failed") - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + verification, err := sso.VerifyAccessToken(token) + if err != nil { + utils.Log.WithError(err).Warn("auth: token verification failed") + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // if verification.UserID == 0 { - // return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") - // } + if verification.UserID == 0 { + return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") + } - // if err := ensureNotRevoked(c, token, verification); err != nil { - // return err - // } + if err := ensureNotRevoked(c, token, verification); err != nil { + return err + } - // user, err := userService.GetBySSOUserID(c, verification.UserID) - // if err != nil || user == nil { - // utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") - // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - // } + user, err := userService.GetBySSOUserID(c, verification.UserID) + if err != nil || user == nil { + utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") + return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } - // if len(requiredScopes) > 0 { - // if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { - // return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") - // } - // } + if len(requiredScopes) > 0 { + if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { + return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") + } + } - // var roles []sso.Role - // permissions := make(map[string]struct{}) - // if verification.UserID != 0 { - // if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { - // utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") - // } else if profile != nil { - // roles = profile.Roles - // for _, perm := range profile.PermissionNames() { - // if perm != "" { - // permissions[perm] = struct{}{} - // } - // } - // } - // } + var roles []sso.Role + permissions := make(map[string]struct{}) + if verification.UserID != 0 { + if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { + utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") + } else if profile != nil { + roles = profile.Roles + for _, perm := range profile.PermissionNames() { + if perm != "" { + permissions[perm] = struct{}{} + } + } + } + } - // ctx := &AuthContext{ - // Token: token, - // Verification: verification, - // User: user, - // Roles: roles, - // Permissions: permissions, - // } + ctx := &AuthContext{ + Token: token, + Verification: verification, + User: user, + Roles: roles, + Permissions: permissions, + } - // c.Locals(authContextLocalsKey, ctx) - // c.Locals(authUserLocalsKey, user) + c.Locals(authContextLocalsKey, ctx) + c.Locals(authUserLocalsKey, user) return c.Next() } }