mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
388 lines
11 KiB
Go
388 lines
11 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
|
|
}
|
|
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: "-",
|
|
}
|
|
|
|
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", "mutasi keluar", "penjualan":
|
|
price := row.UnitPrice
|
|
if price == 0 {
|
|
price = item.Harga
|
|
}
|
|
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
|
|
}
|