mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 14:55:42 +00:00
Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment
This commit is contained in:
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
@@ -13,12 +14,14 @@ import (
|
||||
)
|
||||
|
||||
type ClosingController struct {
|
||||
ClosingService service.ClosingService
|
||||
ClosingService service.ClosingService
|
||||
SapronakService service.SapronakService
|
||||
}
|
||||
|
||||
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||
return &ClosingController{
|
||||
ClosingService: closingService,
|
||||
ClosingService: closingService,
|
||||
SapronakService: sapronakService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,17 +42,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all closings successfully",
|
||||
Message: "Retrieved closing projects list successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToClosingListDTOs(result),
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,3 +126,122 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetOverhead(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.GetOverhead(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get overhead successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
query := &validation.ClosingSapronakQuery{
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Retrieved closing report (sapronak) successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
flag := c.Query("flag", "")
|
||||
|
||||
projectID, err := strconv.Atoi(param)
|
||||
if err != nil || projectID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get perhitungan sapronak per project successfully",
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
flag := c.Query("flag", "")
|
||||
|
||||
projectID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get perhitungan sapronak per kandang successfully",
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,20 +27,35 @@ type ClosingDetailDTO struct {
|
||||
ClosingListDTO
|
||||
}
|
||||
|
||||
type ClosingListItemDTO struct {
|
||||
Id uint `json:"id"`
|
||||
LocationID uint `json:"location_id"`
|
||||
LocationName string `json:"location_name"`
|
||||
ProjectCategory string `json:"project_category"`
|
||||
Period int `json:"period"`
|
||||
ClosingDate string `json:"closing_date"`
|
||||
ShedLabel string `json:"shed_label"`
|
||||
ShedCount int `json:"shed_count"`
|
||||
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
ProjectStatus string `json:"project_status"`
|
||||
}
|
||||
|
||||
type ClosingSummaryDTO struct {
|
||||
LocationID uint `json:"location_id"`
|
||||
Periode int `json:"periode"`
|
||||
JenisProduk string `json:"jenis_produk"`
|
||||
LabelPopulasi string `json:"label_populasi"`
|
||||
JumlahPopulasi int `json:"jumlah_populasi"`
|
||||
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
|
||||
JenisProject string `json:"jenis_project"`
|
||||
KandangAktif int `json:"kandang_aktif"`
|
||||
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
|
||||
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
|
||||
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||
StatusProject string `json:"status_project"`
|
||||
StatusClosing string `json:"status_closing"`
|
||||
FlockID uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
// JenisProduk string `json:"jenis_produk"`
|
||||
// LabelPopulasi string `json:"label_populasi"`
|
||||
Population int `json:"population"`
|
||||
PopulationFormatted string `json:"population_formatted"`
|
||||
ProjectType string `json:"project_type"`
|
||||
ActiveHouseCount int `json:"active_house_count"`
|
||||
ActiveHouseLabel string `json:"active_house_label"`
|
||||
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||
StatusProject string `json:"project_status"`
|
||||
StatusClosing string `json:"closing_status"`
|
||||
}
|
||||
|
||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||
@@ -52,19 +67,38 @@ func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosi
|
||||
populationInt := int(population)
|
||||
|
||||
return ClosingSummaryDTO{
|
||||
LocationID: project.LocationId,
|
||||
Periode: period,
|
||||
JenisProduk: project.Category,
|
||||
LabelPopulasi: "",
|
||||
JumlahPopulasi: populationInt,
|
||||
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||
JenisProject: "",
|
||||
KandangAktif: kandangCount,
|
||||
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
|
||||
StatusPembayaranPenjualan: "Tempo",
|
||||
StatusPembayaranMitra: "",
|
||||
StatusProject: statusProject,
|
||||
StatusClosing: statusClosing,
|
||||
FlockID: project.Id,
|
||||
Period: period,
|
||||
// JenisProduk: project.Category,
|
||||
// LabelPopulasi: "",
|
||||
Population: populationInt,
|
||||
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||
ProjectType: project.Category,
|
||||
ActiveHouseCount: kandangCount,
|
||||
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
||||
SalesPaymentStatus: "Tempo",
|
||||
// StatusPembayaranMitra: "",
|
||||
StatusProject: statusProject,
|
||||
StatusClosing: statusClosing,
|
||||
}
|
||||
}
|
||||
|
||||
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
||||
shedCount := len(project.KandangHistory)
|
||||
|
||||
return ClosingListItemDTO{
|
||||
Id: project.Id,
|
||||
LocationID: project.LocationId,
|
||||
LocationName: project.Location.Name,
|
||||
ProjectCategory: project.Category,
|
||||
Period: maxPeriod(project.KandangHistory),
|
||||
ClosingDate: "17-Nov-2025",
|
||||
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||
ShedCount: shedCount,
|
||||
SalesPaidAmount: 21993726,
|
||||
SalesRemainingAmount: 11075919,
|
||||
SalesPaymentStatus: "Lunas",
|
||||
ProjectStatus: projectStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||
)
|
||||
|
||||
// === Response DTO ===
|
||||
|
||||
type SalesDTO struct {
|
||||
Id uint `json:"id"`
|
||||
RealizationDate time.Time `json:"realization_date"`
|
||||
@@ -29,10 +28,7 @@ type SalesDTO struct {
|
||||
}
|
||||
|
||||
type PenjualanRealisasiResponseDTO struct {
|
||||
ProjectType string `json:"project_type"`
|
||||
FlockId uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
@@ -88,12 +84,10 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||
}
|
||||
|
||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
period := extractPeriodFromRealisasi(e)
|
||||
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
ProjectType: projectType,
|
||||
FlockId: projectFlockID,
|
||||
Period: period,
|
||||
Sales: ToSalesDTOs(e),
|
||||
|
||||
Sales: ToSalesDTOs(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type OverheadDTO struct {
|
||||
ItemName string `json:"item_name"`
|
||||
UOMName string `json:"uom_name"`
|
||||
BudgetQuantity float64 `json:"budget_quantity"`
|
||||
BudgetUnitPrice float64 `json:"budget_unit_price"`
|
||||
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||
ActualDate string `json:"actual_date"`
|
||||
ActualQuantity float64 `json:"actual_quantity"`
|
||||
ActualUnitPrice float64 `json:"actual_unit_price"`
|
||||
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||
CostPerBird float64 `json:"cost_per_bird"`
|
||||
}
|
||||
|
||||
type TotalDTO struct {
|
||||
BudgetQuantity float64 `json:"budget_quantity"`
|
||||
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||
ActualQuantity float64 `json:"actual_quantity"`
|
||||
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||
CostPerBird float64 `json:"cost_per_bird"`
|
||||
}
|
||||
|
||||
type OverheadListDTO struct {
|
||||
Total TotalDTO `json:"total"`
|
||||
Overheads []OverheadDTO `json:"overheads"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
|
||||
if budget == nil && realization == nil {
|
||||
return OverheadDTO{}
|
||||
}
|
||||
|
||||
var itemName, itemUOM string
|
||||
if budget != nil {
|
||||
itemName, itemUOM = getItemInfo(budget.Nonstock)
|
||||
}
|
||||
|
||||
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
|
||||
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
|
||||
}
|
||||
|
||||
dto := OverheadDTO{
|
||||
ItemName: itemName,
|
||||
UOMName: itemUOM,
|
||||
}
|
||||
|
||||
if budget != nil {
|
||||
dto.BudgetQuantity = budget.Qty
|
||||
dto.BudgetUnitPrice = budget.Price
|
||||
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
|
||||
}
|
||||
|
||||
if realization != nil {
|
||||
dto.ActualQuantity = realization.Qty
|
||||
dto.ActualUnitPrice = realization.Price
|
||||
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
|
||||
dto.ActualDate = formatRealizationDate(realization)
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||
latestDateByNonstockID := make(map[uint]string)
|
||||
|
||||
for i := range budgets {
|
||||
nonstockID := budgets[i].NonstockId
|
||||
if overheadsByNonstockID[nonstockID] == nil {
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||
}
|
||||
|
||||
for i := range realizations {
|
||||
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
|
||||
if overheadsByNonstockID[nonstockID] == nil {
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||
|
||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
}
|
||||
|
||||
realizationDateStr := formatRealizationDate(&realizations[i])
|
||||
if realizationDateStr != "" {
|
||||
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
|
||||
latestDateByNonstockID[nonstockID] = realizationDateStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
|
||||
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
|
||||
|
||||
for nonstockID, overhead := range overheadsByNonstockID {
|
||||
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||
|
||||
if overhead.ActualQuantity > 0 {
|
||||
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||
}
|
||||
|
||||
totalBudgetQuantity += overhead.BudgetQuantity
|
||||
totalBudgetAmount += overhead.BudgetTotalAmount
|
||||
totalActualQuantity += overhead.ActualQuantity
|
||||
totalActualAmount += overhead.ActualTotalAmount
|
||||
|
||||
overheadItems = append(overheadItems, *overhead)
|
||||
}
|
||||
|
||||
return OverheadListDTO{
|
||||
Total: TotalDTO{
|
||||
BudgetQuantity: totalBudgetQuantity,
|
||||
BudgetTotalAmount: totalBudgetAmount,
|
||||
ActualQuantity: totalActualQuantity,
|
||||
ActualTotalAmount: totalActualAmount,
|
||||
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||
},
|
||||
Overheads: overheadItems,
|
||||
}
|
||||
}
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||
if nonstock != nil && nonstock.Id != 0 {
|
||||
return nonstock.Name, nonstock.Uom.Name
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func calculateTotal(qty, price float64) float64 {
|
||||
return qty * price
|
||||
}
|
||||
|
||||
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||
if totalChickinQty > 0 {
|
||||
return totalPrice / totalChickinQty
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func formatRealizationDate(realization *entity.ExpenseRealization) string {
|
||||
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
|
||||
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
|
||||
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SapronakDetailDTO struct {
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Flag string `json:"flag"`
|
||||
Tanggal *time.Time `json:"tanggal,omitempty"`
|
||||
NoReferensi string `json:"no_referensi,omitempty"`
|
||||
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
||||
QtyMasuk float64 `json:"qty_masuk"`
|
||||
QtyKeluar float64 `json:"qty_keluar"`
|
||||
Harga float64 `json:"harga"`
|
||||
Nilai float64 `json:"nilai"`
|
||||
}
|
||||
|
||||
type SapronakGroupDTO struct {
|
||||
Flag string `json:"flag"`
|
||||
Items []SapronakDetailDTO `json:"items"`
|
||||
TotalMasuk float64 `json:"total_masuk"`
|
||||
TotalKeluar float64 `json:"total_keluar"`
|
||||
SaldoAkhir float64 `json:"saldo_akhir"`
|
||||
TotalNilai float64 `json:"total_nilai"`
|
||||
}
|
||||
|
||||
type SapronakItemDTO struct {
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Flag string `json:"flag"`
|
||||
IncomingQty float64 `json:"incoming_qty"`
|
||||
IncomingValue float64 `json:"incoming_value"`
|
||||
UsageQty float64 `json:"usage_qty"`
|
||||
UsageValue float64 `json:"usage_value"`
|
||||
RemainingQty float64 `json:"remaining_qty"`
|
||||
AveragePrice float64 `json:"average_price"`
|
||||
}
|
||||
|
||||
type SapronakReportDTO struct {
|
||||
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||
ProjectFlockID uint `json:"project_flock_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
KandangID uint `json:"kandang_id"`
|
||||
KandangName string `json:"kandang_name"`
|
||||
Period int `json:"period"`
|
||||
Status string `json:"status"`
|
||||
StartDate *time.Time `json:"start_date,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
TotalIncomingValue float64 `json:"total_incoming_value"`
|
||||
TotalUsageValue float64 `json:"total_usage_value"`
|
||||
Items []SapronakItemDTO `json:"items"`
|
||||
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// Simplified view for project-level sapronak response
|
||||
type SapronakCategoryRowDTO struct {
|
||||
ID int `json:"id"`
|
||||
Date string `json:"date"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
QtyIn float64 `json:"qty_in"`
|
||||
QtyOut float64 `json:"qty_out"`
|
||||
QtyUsed float64 `json:"qty_used"`
|
||||
Description string `json:"description"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
type SapronakCategoryTotalDTO struct {
|
||||
Label string `json:"label"`
|
||||
QtyIn float64 `json:"qty_in"`
|
||||
QtyOut float64 `json:"qty_out"`
|
||||
QtyUsed float64 `json:"qty_used"`
|
||||
AvgUnitPrice float64 `json:"avg_unit_price"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
}
|
||||
|
||||
type SapronakCategoryDTO struct {
|
||||
Rows []SapronakCategoryRowDTO `json:"rows"`
|
||||
Total SapronakCategoryTotalDTO `json:"total"`
|
||||
}
|
||||
|
||||
type SapronakProjectAggregatedDTO struct {
|
||||
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
||||
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
||||
}
|
||||
|
||||
type ClosingSapronakItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Date string `json:"date"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
ProductSubCategory string `json:"product_sub_category"`
|
||||
SourceWarehouse string `json:"source_warehouse"`
|
||||
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
||||
// Destination string `json:"destination,omitempty"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Unit string `json:"unit"`
|
||||
FormattedQuantity string `json:"formatted_quantity"`
|
||||
Notes string `json:"notes"`
|
||||
SortDate time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type ClosingSapronakDTO struct {
|
||||
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||
}
|
||||
|
||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if len(reports) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
rep := reports[0]
|
||||
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||
}
|
||||
|
||||
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if report == nil {
|
||||
report = &SapronakReportDTO{}
|
||||
}
|
||||
|
||||
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||
|
||||
byFlag := map[string]**SapronakCategoryDTO{}
|
||||
if filter == "" || filter == "DOC" {
|
||||
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["DOC"] = &result.Doc
|
||||
}
|
||||
if filter == "" || filter == "OVK" {
|
||||
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["OVK"] = &result.Ovk
|
||||
}
|
||||
if filter == "" || filter == "PAKAN" {
|
||||
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["PAKAN"] = &result.Pakan
|
||||
}
|
||||
if filter == "" || filter == "PULLET" {
|
||||
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["PULLET"] = &result.Pullet
|
||||
}
|
||||
|
||||
formatDate := func(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("02-Jan-2006")
|
||||
}
|
||||
|
||||
for _, group := range report.Groups {
|
||||
flagKey := strings.ToUpper(group.Flag)
|
||||
ptr := byFlag[flagKey]
|
||||
if ptr == nil || *ptr == nil {
|
||||
continue
|
||||
}
|
||||
target := *ptr
|
||||
|
||||
rowIndexByProduct := make(map[string]int)
|
||||
|
||||
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
||||
if idx, ok := rowIndexByProduct[productKey]; ok {
|
||||
return &target.Rows[idx]
|
||||
}
|
||||
target.Rows = append(target.Rows, base)
|
||||
idx := len(target.Rows) - 1
|
||||
rowIndexByProduct[productKey] = idx
|
||||
return &target.Rows[idx]
|
||||
}
|
||||
|
||||
for idx, item := range group.Items {
|
||||
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
||||
baseRow := SapronakCategoryRowDTO{
|
||||
ID: idx + 1,
|
||||
Date: formatDate(item.Tanggal),
|
||||
ReferenceNumber: item.NoReferensi,
|
||||
Description: item.ProductName,
|
||||
ProductCategory: item.ProductName,
|
||||
UnitPrice: item.Harga,
|
||||
Notes: "-",
|
||||
}
|
||||
|
||||
row := getOrCreateRow(productKey, baseRow)
|
||||
|
||||
switch strings.ToLower(item.JenisTransaksi) {
|
||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
case "pemakaian", "adjustment keluar":
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
case "mutasi keluar":
|
||||
row.QtyOut += item.QtyKeluar
|
||||
default:
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
}
|
||||
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
}
|
||||
}
|
||||
|
||||
for i := range target.Rows {
|
||||
target.Rows[i].ID = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
||||
if cat == nil {
|
||||
return
|
||||
}
|
||||
var qtyIn, qtyOut, qtyUsed, total float64
|
||||
for _, r := range cat.Rows {
|
||||
qtyIn += r.QtyIn
|
||||
qtyOut += r.QtyOut
|
||||
qtyUsed += r.QtyUsed
|
||||
total += r.TotalAmount
|
||||
}
|
||||
avg := 0.0
|
||||
if qtyIn > 0 {
|
||||
avg = total / qtyIn
|
||||
}
|
||||
cat.Total = SapronakCategoryTotalDTO{
|
||||
Label: label,
|
||||
QtyIn: qtyIn,
|
||||
QtyOut: qtyOut,
|
||||
QtyUsed: qtyUsed,
|
||||
AvgUnitPrice: avg,
|
||||
TotalAmount: total,
|
||||
}
|
||||
}
|
||||
|
||||
buildTotals(result.Doc, "TOTAL DOC")
|
||||
buildTotals(result.Ovk, "TOTAL OVK")
|
||||
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||
buildTotals(result.Pullet, "TOTAL PULLET")
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
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"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
@@ -22,13 +24,18 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
closingRepo := rClosing.NewClosingRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||
chickinRepo := rChickin.NewChickinRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ClosingRoutes(router, userService, closingService)
|
||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
}
|
||||
|
||||
type ClosingRepositoryImpl struct {
|
||||
@@ -19,3 +35,566 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
||||
}
|
||||
}
|
||||
|
||||
type SapronakRow struct {
|
||||
Id uint64 `gorm:"column:id"`
|
||||
SortDate time.Time `gorm:"column:sort_date"`
|
||||
DateText string `gorm:"column:date_text"`
|
||||
ReferenceNumber string `gorm:"column:reference_number"`
|
||||
TransactionType string `gorm:"column:transaction_type"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
ProductCategory string `gorm:"column:product_category"`
|
||||
ProductSubCategory string `gorm:"column:product_sub_category"`
|
||||
SourceWarehouse string `gorm:"column:source_warehouse"`
|
||||
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||
Destination string `gorm:"column:destination"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
Unit string `gorm:"column:unit"`
|
||||
Notes string `gorm:"column:notes"`
|
||||
}
|
||||
|
||||
type SapronakQueryParams struct {
|
||||
Type string
|
||||
WarehouseIDs []uint
|
||||
ProjectFlockKandangIDs []uint
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
var (
|
||||
unionParts []string
|
||||
args []any
|
||||
)
|
||||
|
||||
switch params.Type {
|
||||
case validation.SapronakTypeIncoming:
|
||||
if len(params.WarehouseIDs) == 0 {
|
||||
return []SapronakRow{}, 0, nil
|
||||
}
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
case validation.SapronakTypeOutgoing:
|
||||
if len(params.WarehouseIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs)
|
||||
}
|
||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||
args = append(args, params.ProjectFlockKandangIDs)
|
||||
}
|
||||
if len(unionParts) == 0 {
|
||||
return []SapronakRow{}, 0, nil
|
||||
}
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||
}
|
||||
|
||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||
|
||||
var totalResults int64
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||
|
||||
var rows []SapronakRow
|
||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, totalResults, nil
|
||||
}
|
||||
|
||||
const (
|
||||
sapronakIncomingPurchasesSQL = `
|
||||
SELECT
|
||||
CAST(pi.id AS BIGINT) AS id,
|
||||
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||
COALESCE(p.po_number, '') AS reference_number,
|
||||
'Purchase' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
'External Supplier' AS source_warehouse,
|
||||
w.name AS destination_warehouse,
|
||||
'' AS destination,
|
||||
pi.total_qty AS quantity,
|
||||
u.name AS unit,
|
||||
COALESCE(p.notes, '') AS notes
|
||||
FROM purchase_items pi
|
||||
JOIN purchases p ON p.id = pi.purchase_id
|
||||
JOIN products prod ON prod.id = pi.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||
WHERE pi.warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakIncomingTransfersSQL = `
|
||||
SELECT
|
||||
CAST(st.id AS BIGINT) AS id,
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer In' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.quantity AS quantity,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.to_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakOutgoingTransfersSQL = `
|
||||
SELECT
|
||||
CAST(st.id AS BIGINT) AS id,
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer Out' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
COALESCE(tw.name, '') AS destination,
|
||||
std.quantity AS quantity,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.from_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakOutgoingMarketingsSQL = `
|
||||
SELECT
|
||||
CAST(mp.id AS BIGINT) AS id,
|
||||
m.so_date AS sort_date,
|
||||
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||
m.so_number AS reference_number,
|
||||
'Trading Sales' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
w.name AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
'RETAIL CUSTOMER' AS destination,
|
||||
mp.qty AS quantity,
|
||||
u.name AS unit,
|
||||
m.notes AS notes
|
||||
FROM marketing_products mp
|
||||
JOIN marketings m ON m.id = mp.marketing_id
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
WHERE pw.project_flock_kandang_id IN ?
|
||||
`
|
||||
)
|
||||
|
||||
type SapronakIncomingRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
Value float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
type SapronakUsageRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
type SapronakDetailRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Date *time.Time
|
||||
Reference string
|
||||
QtyIn float64
|
||||
QtyOut float64
|
||||
Price float64
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||
|
||||
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||
for _, j := range joins {
|
||||
if strings.TrimSpace(j) != "" {
|
||||
db = db.Joins(j)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func sapronakFlags(flags ...utils.FlagType) []string {
|
||||
out := make([]string, len(flags))
|
||||
for i, f := range flags {
|
||||
out[i] = string(f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var (
|
||||
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
|
||||
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
|
||||
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
|
||||
)
|
||||
|
||||
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
|
||||
m := make(map[uint][]SapronakDetailRow)
|
||||
for _, row := range rows {
|
||||
m[row.ProductID] = append(m[row.ProductID], row)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
|
||||
rows := make([]SapronakDetailRow, 0)
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groupSapronakDetails(rows), nil
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Usage (summary + details)
|
||||
// =========================
|
||||
|
||||
func (r *ClosingRepositoryImpl) usageQuery(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
where string,
|
||||
args ...any,
|
||||
) *gorm.DB {
|
||||
db := r.withCtx(ctx).Table(table).Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(usage_qty), 0) AS qty,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`)
|
||||
db = applyJoins(db, joins...)
|
||||
return db.
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where(where, args...)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
where string,
|
||||
args ...any,
|
||||
) ([]SapronakUsageRow, error) {
|
||||
rows := make([]SapronakUsageRow, 0)
|
||||
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
|
||||
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) detailQuery(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
selectSQL string,
|
||||
where string,
|
||||
args ...any,
|
||||
) *gorm.DB {
|
||||
db := r.withCtx(ctx).
|
||||
Table(table).
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||
|
||||
db = applyJoins(db, joins...)
|
||||
return db.Select(selectSQL).Where(where, args...)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
selectSQL string,
|
||||
where string,
|
||||
args ...any,
|
||||
) (map[uint][]SapronakDetailRow, error) {
|
||||
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||
if pfkID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return r.fetchSapronakUsage(
|
||||
ctx,
|
||||
"recording_stocks rs",
|
||||
"pw.id = rs.product_warehouse_id",
|
||||
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsUsage,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||
if pfkID == 0 {
|
||||
return []SapronakUsageRow{}, nil
|
||||
}
|
||||
return r.fetchSapronakUsage(
|
||||
ctx,
|
||||
"project_chickins pc",
|
||||
"pw.id = pc.product_warehouse_id",
|
||||
nil,
|
||||
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsChickin,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return r.fetchSapronakDetails(
|
||||
ctx,
|
||||
"recording_stocks rs",
|
||||
"pw.id = rs.product_warehouse_id",
|
||||
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
|
||||
`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
r.record_datetime AS date,
|
||||
CAST(r.id AS TEXT) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||
COALESCE(p.product_price,0) AS price
|
||||
`,
|
||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsUsage,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return r.fetchSapronakDetails(
|
||||
ctx,
|
||||
"project_chickins pc",
|
||||
"pw.id = pc.product_warehouse_id",
|
||||
nil,
|
||||
`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
pc.chick_in_date AS date,
|
||||
CAST(pc.id AS TEXT) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(pc.usage_qty,0) AS qty_out,
|
||||
COALESCE(p.product_price,0) AS price
|
||||
`,
|
||||
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsChickin,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||
return r.withCtx(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||
Joins("JOIN products p ON p.id = pi.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where("pi.received_date IS NOT NULL")
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
|
||||
rows := make([]SapronakIncomingRow, 0)
|
||||
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||
pi.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`)
|
||||
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return scanAndGroupDetails(
|
||||
r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||
pi.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
pi.received_date AS date,
|
||||
COALESCE(po.po_number, '') AS reference,
|
||||
COALESCE(pi.total_qty,0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(pi.price,0) AS price
|
||||
`),
|
||||
)
|
||||
}
|
||||
|
||||
type stockLogSapronakRow struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
Flag string `gorm:"column:flag"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
Increase float64 `gorm:"column:increase"`
|
||||
Decrease float64 `gorm:"column:decrease"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
MovementNumber string `gorm:"column:movement_number"`
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
|
||||
rows := make([]stockLogSapronakRow, 0)
|
||||
|
||||
movementSelect := "'' AS movement_number"
|
||||
joins := []string{}
|
||||
if withMovement {
|
||||
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
|
||||
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
|
||||
}
|
||||
|
||||
db := r.withCtx(ctx).
|
||||
Table("stock_logs sl").
|
||||
Select(`
|
||||
sl.id AS id,
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
sl.created_at AS created_at,
|
||||
COALESCE(sl.increase,0) AS increase,
|
||||
COALESCE(sl.decrease,0) AS decrease,
|
||||
COALESCE(p.product_price,0) AS price,
|
||||
` + movementSelect + `
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
|
||||
|
||||
db = applyJoins(db, joins...)
|
||||
|
||||
if err := db.
|
||||
Where("sl.loggable_type = ?", logType).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
|
||||
in := make(map[uint][]SapronakDetailRow)
|
||||
out := make(map[uint][]SapronakDetailRow)
|
||||
|
||||
for _, row := range rows {
|
||||
base := SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: refFn(row),
|
||||
Price: row.Price,
|
||||
}
|
||||
|
||||
if row.Increase > 0 {
|
||||
d := base
|
||||
d.QtyIn = row.Increase
|
||||
in[row.ProductID] = append(in[row.ProductID], d)
|
||||
}
|
||||
if row.Decrease > 0 {
|
||||
d := base
|
||||
d.QtyOut = row.Decrease
|
||||
out[row.ProductID] = append(out[row.ProductID], d)
|
||||
}
|
||||
}
|
||||
|
||||
return in, out
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||
return in, out, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||
return ref
|
||||
}
|
||||
return fmt.Sprintf("TRF-%d", row.ID)
|
||||
})
|
||||
return in, out, nil
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
||||
ctrl := controller.NewClosingController(s)
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||
|
||||
route := v1.Group("/closing")
|
||||
route := v1.Group("/closings")
|
||||
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)
|
||||
@@ -23,4 +24,11 @@ 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)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
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"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
@@ -22,10 +25,12 @@ import (
|
||||
)
|
||||
|
||||
type ClosingService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
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)
|
||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||
}
|
||||
|
||||
type closingService struct {
|
||||
@@ -36,9 +41,12 @@ type closingService struct {
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, 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, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -47,6 +55,9 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||
ProjectBudgetRepo: projectBudgetRepo,
|
||||
ChickinRepo: chickinRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +67,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
|
||||
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db).
|
||||
Preload("Location").
|
||||
Preload("KandangHistory").
|
||||
Preload("KandangHistory.Chickins")
|
||||
}
|
||||
|
||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -68,9 +80,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
db = s.withClosingRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -79,7 +91,19 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
s.Log.Errorf("Failed to get closings: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return closings, total, nil
|
||||
|
||||
result := make([]dto.ClosingListItemDTO, 0, len(closings))
|
||||
for _, closing := range closings {
|
||||
statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||
}
|
||||
|
||||
result = append(result, dto.ToClosingListItemDTO(closing, statusProject))
|
||||
}
|
||||
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||
@@ -144,6 +168,147 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
params = &validation.ClosingSapronakQuery{}
|
||||
}
|
||||
|
||||
if params.Page == 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Limit == 0 {
|
||||
params.Limit = 10
|
||||
}
|
||||
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||
}
|
||||
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||
}
|
||||
|
||||
var projectFlockKandangIDs []uint
|
||||
if params.Type == validation.SapronakTypeOutgoing {
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{
|
||||
Type: params.Type,
|
||||
WarehouseIDs: warehouseIDs,
|
||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||
Limit: params.Limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data")
|
||||
}
|
||||
|
||||
items := make([]dto.ClosingSapronakItemDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
dateStr := row.DateText
|
||||
if dateStr == "" && !row.SortDate.IsZero() {
|
||||
dateStr = row.SortDate.Format("02-Jan-2006")
|
||||
}
|
||||
items = append(items, dto.ClosingSapronakItemDTO{
|
||||
Id: row.Id,
|
||||
Date: dateStr,
|
||||
ReferenceNumber: row.ReferenceNumber,
|
||||
TransactionType: row.TransactionType,
|
||||
ProductName: row.ProductName,
|
||||
ProductCategory: row.ProductCategory,
|
||||
ProductSubCategory: row.ProductSubCategory,
|
||||
SourceWarehouse: row.SourceWarehouse,
|
||||
DestinationWarehouse: row.DestinationWarehouse,
|
||||
// Destination: row.Destination,
|
||||
Quantity: row.Quantity,
|
||||
Unit: row.Unit,
|
||||
FormattedQuantity: formatQuantity(row.Quantity, row.Unit),
|
||||
Notes: row.Notes,
|
||||
SortDate: row.SortDate,
|
||||
})
|
||||
}
|
||||
|
||||
return items, totalResults, nil
|
||||
}
|
||||
|
||||
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
var kandangIDs []uint
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
|
||||
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("kandang_id", &kandangIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(kandangIDs) == 0 {
|
||||
return []uint{}, nil
|
||||
}
|
||||
|
||||
var warehouses []entity.Warehouse
|
||||
if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unique := make(map[uint]struct{})
|
||||
for _, warehouse := range warehouses {
|
||||
unique[warehouse.Id] = struct{}{}
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(unique))
|
||||
for id := range unique {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func formatQuantity(qty float64, uom string) string {
|
||||
qtyStr := strconv.FormatFloat(qty, 'f', -1, 64)
|
||||
if uom == "" {
|
||||
return qtyStr
|
||||
}
|
||||
return qtyStr + " " + uom
|
||||
}
|
||||
|
||||
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||
if s.ApprovalSvc == nil {
|
||||
return "", "Belum Selesai", nil
|
||||
@@ -165,7 +330,7 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
minStep = rec.StepNumber
|
||||
statusProject = rec.StepName
|
||||
}
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
@@ -188,3 +353,29 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
|
||||
return statusProject, statusClosing, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalChickinQty float64
|
||||
for _, chickin := range chickins {
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,681 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type SapronakService interface {
|
||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||
}
|
||||
|
||||
type sapronakService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewSapronakService(
|
||||
repo repository.ClosingRepository,
|
||||
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||
validate *validator.Validate,
|
||||
) SapronakService {
|
||||
return &sapronakService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockKandangRepo: pfkRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||
}
|
||||
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
Status: "all",
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) <= 1 {
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||
return []dto.SapronakReportDTO{combined}, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||
if projectFlockID == 0 || pfkID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||
}
|
||||
|
||||
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
ProjectFlockKandangID: pfkID,
|
||||
Status: "all",
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||
return &res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||
}
|
||||
|
||||
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pfks) == 0 {
|
||||
return []dto.SapronakReportDTO{}, nil
|
||||
}
|
||||
|
||||
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||
if filterStatus == "" {
|
||||
filterStatus = "all"
|
||||
}
|
||||
|
||||
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||
for _, pfk := range pfks {
|
||||
status := "closing"
|
||||
if pfk.ClosedAt == nil {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||
continue
|
||||
}
|
||||
|
||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||
}
|
||||
|
||||
results = append(results, dto.SapronakReportDTO{
|
||||
ProjectFlockKandangID: pfk.Id,
|
||||
ProjectFlockID: pfk.ProjectFlockId,
|
||||
ProjectName: pfk.ProjectFlock.FlockName,
|
||||
KandangID: pfk.KandangId,
|
||||
KandangName: pfk.Kandang.Name,
|
||||
Period: pfk.Period,
|
||||
Status: status,
|
||||
StartDate: nil,
|
||||
EndDate: nil,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("Kandang").
|
||||
Preload("Chickins")
|
||||
|
||||
if params != nil {
|
||||
if params.ProjectFlockID > 0 {
|
||||
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||
}
|
||||
if params.KandangID > 0 {
|
||||
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||
}
|
||||
if params.ProjectFlockKandangID > 0 {
|
||||
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
|
||||
}
|
||||
}
|
||||
|
||||
var pfks []entity.ProjectFlockKandang
|
||||
if err := db.Find(&pfks).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||
}
|
||||
return pfks, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||
if len(reports) == 0 {
|
||||
return dto.SapronakReportDTO{}
|
||||
}
|
||||
|
||||
var (
|
||||
totalIncoming float64
|
||||
totalUsage float64
|
||||
projectName = reports[0].ProjectName
|
||||
)
|
||||
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO)
|
||||
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
return g
|
||||
}
|
||||
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||
return groupMap[flag]
|
||||
}
|
||||
|
||||
for _, r := range reports {
|
||||
totalIncoming += r.TotalIncomingValue
|
||||
totalUsage += r.TotalUsageValue
|
||||
|
||||
for _, it := range r.Items {
|
||||
cur := itemMap[it.ProductID]
|
||||
if cur.ProductID == 0 {
|
||||
cur.ProductID = it.ProductID
|
||||
cur.ProductName = it.ProductName
|
||||
cur.Flag = it.Flag
|
||||
}
|
||||
cur.IncomingQty += it.IncomingQty
|
||||
cur.IncomingValue += it.IncomingValue
|
||||
cur.UsageQty += it.UsageQty
|
||||
cur.UsageValue += it.UsageValue
|
||||
if cur.IncomingQty >= cur.UsageQty {
|
||||
cur.RemainingQty = cur.IncomingQty - cur.UsageQty
|
||||
} else {
|
||||
cur.RemainingQty = 0
|
||||
}
|
||||
if cur.IncomingQty > 0 {
|
||||
cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
|
||||
} else {
|
||||
cur.AveragePrice = it.AveragePrice
|
||||
}
|
||||
itemMap[it.ProductID] = cur
|
||||
}
|
||||
|
||||
for _, g := range r.Groups {
|
||||
agg := ensureGroup(g.Flag)
|
||||
agg.TotalMasuk += g.TotalMasuk
|
||||
agg.TotalKeluar += g.TotalKeluar
|
||||
agg.SaldoAkhir += g.SaldoAkhir
|
||||
agg.TotalNilai += g.TotalNilai
|
||||
agg.Items = append(agg.Items, g.Items...)
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||
for _, it := range itemMap {
|
||||
items = append(items, it)
|
||||
}
|
||||
|
||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
}
|
||||
|
||||
return dto.SapronakReportDTO{
|
||||
ProjectFlockID: projectID,
|
||||
ProjectName: projectName,
|
||||
Status: "combined",
|
||||
StartDate: nil,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
|
||||
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
|
||||
for _, row := range incomingRows {
|
||||
incoming[row.ProductID] = row
|
||||
}
|
||||
usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
|
||||
for _, row := range usageRows {
|
||||
usage[row.ProductID] = row
|
||||
}
|
||||
return incoming, usage
|
||||
}
|
||||
|
||||
type sapronakDetailMaps struct {
|
||||
Incoming map[uint][]dto.SapronakDetailDTO
|
||||
Usage map[uint][]dto.SapronakDetailDTO
|
||||
AdjIncoming map[uint][]dto.SapronakDetailDTO
|
||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||
}
|
||||
|
||||
func buildSapronakDetails(
|
||||
incomingRows map[uint][]repository.SapronakDetailRow,
|
||||
usageRows map[uint][]repository.SapronakDetailRow,
|
||||
adjIncomingRows map[uint][]repository.SapronakDetailRow,
|
||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||
) sapronakDetailMaps {
|
||||
result := sapronakDetailMaps{
|
||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
Usage: make(map[uint][]dto.SapronakDetailDTO),
|
||||
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||
}
|
||||
|
||||
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||
for pid, rows := range src {
|
||||
for _, r := range rows {
|
||||
d := dto.SapronakDetailDTO{
|
||||
ProductID: r.ProductID,
|
||||
ProductName: r.ProductName,
|
||||
Flag: r.Flag,
|
||||
Tanggal: r.Date,
|
||||
NoReferensi: r.Reference,
|
||||
JenisTransaksi: jenis,
|
||||
Harga: r.Price,
|
||||
}
|
||||
if masuk {
|
||||
d.QtyMasuk = r.QtyIn
|
||||
d.Nilai = r.QtyIn * r.Price
|
||||
} else {
|
||||
d.QtyKeluar = r.QtyOut
|
||||
d.Nilai = r.QtyOut * r.Price
|
||||
}
|
||||
target[pid] = append(target[pid], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||
// For sapronak closing report we intentionally ignore date range
|
||||
// and aggregate all historical transactions for the kandang/project.
|
||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||
matchesFlag := func(f string) bool {
|
||||
if filterFlag == "" {
|
||||
return true
|
||||
}
|
||||
return strings.ToUpper(f) == filterFlag
|
||||
}
|
||||
|
||||
// For project flocks with category GROWING, pullet usage from chickin
|
||||
// should not be counted yet. Only when category is LAYING we allow
|
||||
// pullet usage to contribute to qty_used.
|
||||
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
|
||||
|
||||
if !isLaying {
|
||||
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
|
||||
for _, row := range chickinUsageRows {
|
||||
if strings.ToUpper(row.Flag) == "DOC" {
|
||||
filteredUsage = append(filteredUsage, row)
|
||||
}
|
||||
}
|
||||
chickinUsageRows = filteredUsage
|
||||
|
||||
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
|
||||
for pid, rows := range chickinUsageDetailsRows {
|
||||
for _, d := range rows {
|
||||
if strings.ToUpper(d.Flag) == "DOC" {
|
||||
filteredDetail[pid] = append(filteredDetail[pid], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
chickinUsageDetailsRows = filteredDetail
|
||||
}
|
||||
|
||||
allUsageRows := append(usageRows, chickinUsageRows...)
|
||||
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||
|
||||
for pid, rows := range chickinUsageDetailsRows {
|
||||
if len(rows) == 0 {
|
||||
continue
|
||||
}
|
||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||
}
|
||||
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||
incomingDetails := detailMaps.Incoming
|
||||
usageDetails := detailMaps.Usage
|
||||
adjIncoming := detailMaps.AdjIncoming
|
||||
adjOutgoing := detailMaps.AdjOutgoing
|
||||
transIncoming := detailMaps.TransferIn
|
||||
transOutgoing := detailMaps.TransferOut
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
return g
|
||||
}
|
||||
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||
return groupMap[flag]
|
||||
}
|
||||
|
||||
for _, row := range incoming {
|
||||
if !matchesFlag(row.Flag) {
|
||||
continue
|
||||
}
|
||||
avgPrice := row.DefaultPrice
|
||||
if row.Qty > 0 && row.Value > 0 {
|
||||
avgPrice = row.Value / row.Qty
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
IncomingQty: row.Qty,
|
||||
IncomingValue: row.Value,
|
||||
RemainingQty: row.Qty,
|
||||
AveragePrice: avgPrice,
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range usage {
|
||||
if !matchesFlag(row.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[row.ProductID]
|
||||
price := existing.AveragePrice
|
||||
if price == 0 {
|
||||
price = row.DefaultPrice
|
||||
}
|
||||
|
||||
usageValue := row.Qty * price
|
||||
|
||||
existing.ProductID = row.ProductID
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = row.ProductName
|
||||
}
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = row.Flag
|
||||
}
|
||||
existing.AveragePrice = price
|
||||
existing.UsageQty += row.Qty
|
||||
existing.UsageValue += usageValue
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = existing
|
||||
}
|
||||
|
||||
for productID, details := range adjIncoming {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.IncomingQty += d.QtyMasuk
|
||||
existing.IncomingValue += d.Nilai
|
||||
if existing.IncomingQty > 0 {
|
||||
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||
}
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjOutgoing {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.UsageQty += d.QtyKeluar
|
||||
existing.UsageValue += d.Nilai
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transIncoming {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.IncomingQty += d.QtyMasuk
|
||||
existing.IncomingValue += d.Nilai
|
||||
if existing.IncomingQty > 0 {
|
||||
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||
}
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||
var totalIncoming, totalUsage float64
|
||||
for _, item := range itemMap {
|
||||
totalIncoming += item.IncomingValue
|
||||
totalUsage += item.UsageValue
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
for productID, details := range incomingDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range usageDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
}
|
||||
|
||||
return items, groups, totalIncoming, totalUsage, nil
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -13,3 +13,14 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
const (
|
||||
SapronakTypeIncoming = "incoming"
|
||||
SapronakTypeOutgoing = "outgoing"
|
||||
)
|
||||
|
||||
type ClosingSapronakQuery struct {
|
||||
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package validation
|
||||
|
||||
type CountSapronakQuery struct {
|
||||
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=active closing all"`
|
||||
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
|
||||
}
|
||||
Reference in New Issue
Block a user