Files
lti-api/internal/modules/closings/dto/closingSapronak.dto.go
T
2026-04-21 19:30:03 +07:00

413 lines
12 KiB
Go

package dto
import (
"sort"
"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"`
}
type ClosingSapronakSummaryItemDTO struct {
Category string `json:"category"`
TotalQty int64 `json:"total_qty"`
Uom UomSummaryDTO `json:"uom"`
}
type UomSummaryDTO struct {
ID uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if len(reports) == 0 {
return result
}
rep := reports[0]
return ToSapronakProjectAggregatedFromReport(&rep, flag, productFlags)
}
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if report == nil {
report = &SapronakReportDTO{}
}
normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "PULLET" {
return "DOC"
}
return normalized
}
normalizeCutOverToken := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
normalized = strings.ReplaceAll(normalized, "-", "")
normalized = strings.ReplaceAll(normalized, " ", "")
return normalized
}
containsCutOver := func(values ...string) bool {
for _, value := range values {
if strings.Contains(normalizeCutOverToken(value), "CUTOVER") {
return true
}
}
return false
}
filter := normalizeFlag(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
}
formatDate := func(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("02-Jan-2006")
}
flagOrder := map[string]int{
"DOC": 0,
"PAKAN": 0,
"OVK": 0,
"PULLET": 0,
}
buildFlagList := func(productID uint, fallback string) string {
rawFlags := productFlags[productID]
if len(rawFlags) == 0 {
if fallback == "" {
return ""
}
return fallback
}
seen := make(map[string]struct{}, len(rawFlags))
ordered := make([]string, 0, len(rawFlags))
for _, f := range rawFlags {
flagName := strings.ToUpper(strings.TrimSpace(f))
if flagName == "" {
continue
}
if _, ok := seen[flagName]; ok {
continue
}
seen[flagName] = struct{}{}
ordered = append(ordered, flagName)
}
sort.SliceStable(ordered, func(i, j int) bool {
li := ordered[i]
lj := ordered[j]
ri, iok := flagOrder[li]
rj, jok := flagOrder[lj]
if iok != jok {
if iok {
return true
}
return false
}
if iok && jok && ri != rj {
return ri < rj
}
return li < lj
})
return strings.Join(ordered, " ")
}
for _, group := range report.Groups {
flagKey := normalizeFlag(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 {
refKey := strings.TrimSpace(item.NoReferensi)
productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey)
if refKey == "" {
productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal))
}
baseRow := SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
ReferenceNumber: item.NoReferensi,
Description: item.ProductName,
ProductCategory: buildFlagList(item.ProductID, flagKey),
UnitPrice: item.Harga,
Notes: "-",
}
isCutOver := containsCutOver(baseRow.ProductCategory, baseRow.Description, item.ProductName)
row := getOrCreateRow(productKey, baseRow)
switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk
if item.Tanggal != nil {
row.Date = formatDate(item.Tanggal)
}
if row.UnitPrice == 0 {
if item.QtyMasuk > 0 && item.Nilai > 0 {
row.UnitPrice = item.Nilai / item.QtyMasuk
} else if item.Harga > 0 {
row.UnitPrice = item.Harga
}
}
if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
case "pemakaian":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price
case "adjustment keluar":
price := row.UnitPrice
if price == 0 {
price = item.Harga
}
if row.UnitPrice == 0 {
row.UnitPrice = price
}
if isCutOver {
row.QtyUsed += item.QtyKeluar
row.TotalAmount += item.QtyKeluar * price
continue
}
row.QtyOut += item.QtyKeluar
case "mutasi keluar", "penjualan":
row.QtyOut += item.QtyKeluar
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
if strings.HasPrefix(ref, "TL-") {
row.Notes = "TRANSFER LAYING"
} else if strings.HasPrefix(ref, "ST-") {
row.Notes = "TRANSFER STOCK"
}
}
default:
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
}
}
// For chicken categories, keep qty_used aligned with qty_in - qty_out.
// Sales are excluded; usage represents remaining after transfers.
adjustChicken := func(cat *SapronakCategoryDTO) {
if cat == nil {
return
}
for i := range cat.Rows {
row := &cat.Rows[i]
remaining := row.QtyIn - row.QtyOut
if remaining < 0 {
remaining = 0
}
row.QtyUsed = remaining
if row.UnitPrice > 0 {
row.TotalAmount = row.QtyUsed * row.UnitPrice
}
}
}
adjustChicken(result.Doc)
adjustChicken(result.Pullet)
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 qtyUsed > 0 {
avg = total / qtyUsed
}
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")
// For chicken categories, enforce total qty_used = qty_in - qty_out.
adjustChickenTotal := func(cat *SapronakCategoryDTO) {
if cat == nil {
return
}
remaining := cat.Total.QtyIn - cat.Total.QtyOut
if remaining < 0 {
remaining = 0
}
cat.Total.QtyUsed = remaining
if cat.Total.AvgUnitPrice > 0 {
cat.Total.TotalAmount = cat.Total.AvgUnitPrice * remaining
}
}
adjustChickenTotal(result.Doc)
adjustChickenTotal(result.Pullet)
return result
}