Compare commits

..

26 Commits

Author SHA1 Message Date
Giovanni Gabriel Septriadi 621d0d2bfd Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!541
2026-05-19 06:48:41 +00:00
Giovanni Gabriel Septriadi 1fd3f96038 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!532
2026-05-12 09:31:19 +00:00
Giovanni Gabriel Septriadi cf0fc9e7e6 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!530
2026-05-11 08:32:04 +00:00
Adnan Zahir d9041a89bb Merge branch 'fix/chickin' into 'production'
[FIX][BE]: add migration for edit chickin_date pullet cikaum 1 dan pullet cikaum 2

See merge request mbugroup/lti-api!518
2026-05-08 15:05:02 +07:00
giovanni c75281ebd9 add migration for update day recording pullet cikaum 1 dan 2 2026-05-07 17:35:15 +07:00
Adnan Zahir ca3ad810c6 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!505
2026-05-05 14:12:22 +07:00
Adnan Zahir 655b1ad5fe Merge branch 'development' into 'production'
fix: resolve dashboard OpenAPI integration issues

See merge request mbugroup/lti-api!498
2026-05-03 13:08:58 +07:00
Adnan Zahir 84db5fe37a Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!494
2026-04-29 12:53:02 +07:00
Adnan Zahir 63a78da18d Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!480
2026-04-26 00:13:58 +07:00
Adnan Zahir ac50c06cd7 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!478
2026-04-25 15:26:22 +07:00
Adnan Zahir b60649f59d Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!476
2026-04-25 14:16:20 +07:00
Adnan Zahir 6acc9416c1 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!473
2026-04-24 21:24:56 +07:00
Adnan Zahir bb4e5d6e3e Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!469
2026-04-24 14:20:43 +07:00
Adnan Zahir 170c221957 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!467
2026-04-24 13:31:35 +07:00
Adnan Zahir 812327f148 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!461
2026-04-24 12:30:41 +07:00
Adnan Zahir cd192128f1 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!443
2026-04-23 12:38:24 +07:00
Adnan Zahir a5d4d6c11d Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!436
2026-04-22 13:13:06 +07:00
Adnan Zahir 1452f8d083 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!427
2026-04-20 10:16:45 +07:00
Adnan Zahir 33c6706181 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!425
2026-04-20 08:24:44 +07:00
Adnan Zahir c9618e1095 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!422
2026-04-18 09:47:04 +07:00
Adnan Zahir cae7f3ef63 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!414
2026-04-14 12:01:04 +07:00
Adnan Zahir 42793d94bd Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!412
2026-04-13 14:12:48 +07:00
Adnan Zahir 1369bf0e36 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!411
2026-04-11 14:08:35 +07:00
Adnan Zahir 361d14bd3e Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!406
2026-04-10 10:50:07 +07:00
Adnan Zahir 7923352535 Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!401
2026-04-07 22:58:38 +07:00
Adnan Zahir 010240066a Merge branch 'development' into 'production'
Development

See merge request mbugroup/lti-api!399
2026-04-07 16:52:45 +07:00
23 changed files with 249 additions and 2885 deletions
@@ -98,7 +98,6 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
return db.
Preload("Expense").
Preload("Expense.Supplier").
Preload("Expense.Location").
Preload("Kandang").
Preload("Kandang.Location").
Preload("Nonstock").
@@ -5,7 +5,6 @@ import (
"strconv"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations"
@@ -14,8 +13,6 @@ import (
"github.com/gofiber/fiber/v2"
)
const transactionExcelExportFetchLimit = 99999999
type TransactionController struct {
TransactionService service.TransactionService
}
@@ -110,14 +107,6 @@ func (u *TransactionController) GetAll(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if isTransactionExcelExportRequest(c) {
results, err := u.getAllTransactionsForExcel(c, query)
if err != nil {
return err
}
return exportTransactionListExcel(c, results)
}
result, totalResults, err := u.TransactionService.GetAll(c, query)
if err != nil {
return err
@@ -160,32 +149,6 @@ func (u *TransactionController) GetOne(c *fiber.Ctx) error {
})
}
func isTransactionExcelExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
}
func (u *TransactionController) getAllTransactionsForExcel(c *fiber.Ctx, baseQuery *validation.Query) ([]entity.Payment, error) {
query := *baseQuery
query.Page = 1
query.Limit = transactionExcelExportFetchLimit
results := make([]entity.Payment, 0)
for {
pageResults, total, err := u.TransactionService.GetAll(c, &query)
if err != nil {
return nil, err
}
if len(pageResults) == 0 || total == 0 {
break
}
results = append(results, pageResults...)
if int64(len(results)) >= total {
break
}
query.Page++
}
return results, nil
}
func (u *TransactionController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
@@ -1,307 +0,0 @@
package controller
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
const transactionExportSheetName = "Transaksi"
func exportTransactionListExcel(c *fiber.Ctx, payments []entity.Payment) error {
content, err := buildTransactionExportWorkbook(payments)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("transaksi_%s.xlsx", time.Now().Format("20060102_150405"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(content)
}
func buildTransactionExportWorkbook(payments []entity.Payment) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if defaultSheet != transactionExportSheetName {
if err := file.SetSheetName(defaultSheet, transactionExportSheetName); err != nil {
return nil, err
}
}
if err := setTransactionExportColumns(file); err != nil {
return nil, err
}
if err := setTransactionExportHeaders(file); err != nil {
return nil, err
}
if err := setTransactionExportRows(file, payments); err != nil {
return nil, err
}
if err := file.SetPanes(transactionExportSheetName, &excelize.Panes{
Freeze: true,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
}); err != nil {
return nil, err
}
buffer, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func setTransactionExportColumns(file *excelize.File) error {
columnWidths := map[string]float64{
"A": 20,
"B": 22,
"C": 18,
"D": 25,
"E": 14,
"F": 16,
"G": 16,
"H": 22,
"I": 22,
"J": 18,
"K": 18,
"L": 18,
"M": 30,
"N": 22,
"O": 20,
}
sheet := transactionExportSheetName
for col, width := range columnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
return file.SetRowHeight(sheet, 1, 24)
}
func setTransactionExportHeaders(file *excelize.File) error {
sheet := transactionExportSheetName
headers := []string{
"Kode Pembayaran",
"No. Referensi",
"Tipe Transaksi",
"Pihak",
"Tipe Pihak",
"Tanggal Bayar",
"Metode Bayar",
"Bank",
"No. Rekening Bank",
"Pemasukan",
"Pengeluaran",
"Nominal",
"Catatan",
"Dibuat Oleh",
"Status",
}
for i, header := range headers {
colName, err := excelize.ColumnNumberToName(i + 1)
if err != nil {
return err
}
if err := file.SetCellValue(sheet, colName+"1", header); err != nil {
return err
}
}
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "1F2937"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DCEBFA"}},
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
Border: []excelize.Border{
{Type: "left", Color: "D1D5DB", Style: 1},
{Type: "top", Color: "D1D5DB", Style: 1},
{Type: "bottom", Color: "D1D5DB", Style: 1},
{Type: "right", Color: "D1D5DB", Style: 1},
},
})
if err != nil {
return err
}
return file.SetCellStyle(sheet, "A1", "O1", headerStyle)
}
func setTransactionExportRows(file *excelize.File, payments []entity.Payment) error {
if len(payments) == 0 {
return nil
}
sheet := transactionExportSheetName
for i, p := range payments {
row := strconv.Itoa(i + 2)
if err := writeTransactionExportRow(file, sheet, row, p); err != nil {
return err
}
}
lastRow := strconv.Itoa(len(payments) + 1)
dataStyle, err := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true},
Border: []excelize.Border{
{Type: "left", Color: "D1D5DB", Style: 1},
{Type: "top", Color: "D1D5DB", Style: 1},
{Type: "bottom", Color: "D1D5DB", Style: 1},
{Type: "right", Color: "D1D5DB", Style: 1},
},
})
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A2", "O"+lastRow, dataStyle); err != nil {
return err
}
numericStyle, err := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{Horizontal: "right", Vertical: "center"},
Border: []excelize.Border{
{Type: "left", Color: "D1D5DB", Style: 1},
{Type: "top", Color: "D1D5DB", Style: 1},
{Type: "bottom", Color: "D1D5DB", Style: 1},
{Type: "right", Color: "D1D5DB", Style: 1},
},
})
if err != nil {
return err
}
return file.SetCellStyle(sheet, "J2", "L"+lastRow, numericStyle)
}
func writeTransactionExportRow(file *excelize.File, sheet, row string, p entity.Payment) error {
incomeAmount, expenseAmount := txAmounts(p.Direction, p.Nominal)
values := []interface{}{
safeTxText(p.PaymentCode),
safeTxRefNumber(p.ReferenceNumber),
safeTxText(txTransactionType(p)),
safeTxText(txPartyName(p)),
safeTxText(p.PartyType),
formatTxDate(p.PaymentDate),
safeTxText(p.PaymentMethod),
safeTxBank(p),
safeTxBankAccount(p),
incomeAmount,
expenseAmount,
p.Nominal,
safeTxText(p.Notes),
safeTxText(txCreatedBy(p)),
formatTxStatus(p),
}
for colIdx, val := range values {
colName, err := excelize.ColumnNumberToName(colIdx + 1)
if err != nil {
return err
}
if err := file.SetCellValue(sheet, colName+row, val); err != nil {
return err
}
}
return nil
}
func safeTxText(s string) string {
trimmed := strings.TrimSpace(s)
if trimmed == "" {
return "-"
}
return trimmed
}
func safeTxRefNumber(s *string) string {
if s == nil {
return "-"
}
return safeTxText(*s)
}
func safeTxBank(p entity.Payment) string {
if p.BankWarehouse.Id == 0 {
return "-"
}
return safeTxText(p.BankWarehouse.Name)
}
func safeTxBankAccount(p entity.Payment) string {
if p.BankWarehouse.Id == 0 {
return "-"
}
return safeTxText(p.BankWarehouse.AccountNumber)
}
func formatTxDate(t time.Time) string {
if t.IsZero() {
return "-"
}
loc, err := time.LoadLocation("Asia/Jakarta")
if err == nil {
t = t.In(loc)
}
return t.Format("02-01-2006")
}
func formatTxStatus(p entity.Payment) string {
if p.LatestApproval == nil {
return "-"
}
return safeTxText(p.LatestApproval.StepName)
}
func txTransactionType(p entity.Payment) string {
if p.TransactionType != "" {
return p.TransactionType
}
return p.Direction
}
func txPartyName(p entity.Payment) string {
switch p.PartyType {
case "CUSTOMER":
if p.Customer != nil && p.Customer.Id != 0 {
return p.Customer.Name
}
case "SUPPLIER":
if p.Supplier != nil && p.Supplier.Id != 0 {
return p.Supplier.Name
}
}
return ""
}
func txCreatedBy(p entity.Payment) string {
if p.CreatedUser.Id == 0 {
return ""
}
return p.CreatedUser.Name
}
func txAmounts(direction string, nominal float64) (income, expense float64) {
switch strings.ToUpper(direction) {
case "IN":
return nominal, 0
case "OUT":
return 0, nominal
default:
return 0, 0
}
}
@@ -10,7 +10,7 @@ type Update struct {
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
TransactionTypes []string `query:"transaction_types" validate:"omitempty,dive,max=50"`
BankIDs []uint `query:"bank_ids" validate:"omitempty,dive,gt=0"`
@@ -2,6 +2,7 @@ package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
@@ -152,7 +153,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err := file.SetCellValue(sheet, "D"+strconv.Itoa(rowNumber), safeMarketingExportText(item.Customer.Name)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), sumMarketingGrandTotal(item.SalesOrder)); err != nil {
if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), formatMarketingRupiah(sumMarketingGrandTotal(item.SalesOrder))); err != nil {
return err
}
if err := file.SetCellValue(sheet, "F"+strconv.Itoa(rowNumber), formatMarketingProducts(item.SalesOrder)); err != nil {
@@ -265,6 +266,40 @@ func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 {
return total
}
func formatMarketingRupiah(value float64) string {
if math.IsNaN(value) || math.IsInf(value, 0) {
return "Rp 0"
}
rounded := int64(math.Round(value))
sign := ""
if rounded < 0 {
sign = "-"
rounded = -rounded
}
raw := strconv.FormatInt(rounded, 10)
if raw == "" {
raw = "0"
}
var grouped strings.Builder
rem := len(raw) % 3
if rem > 0 {
grouped.WriteString(raw[:rem])
if len(raw) > rem {
grouped.WriteString(".")
}
}
for i := rem; i < len(raw); i += 3 {
grouped.WriteString(raw[i : i+3])
if i+3 < len(raw) {
grouped.WriteString(".")
}
}
return "Rp " + sign + grouped.String()
}
func safeMarketingExportText(value string) string {
trimmed := strings.TrimSpace(value)
@@ -91,9 +91,11 @@ func buildPurchaseQuery(c *fiber.Ctx) *validation.Query {
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search")),
ApprovalStatus: strings.TrimSpace(c.Query("approval_status")),
StartDate: strings.TrimSpace(c.Query("start_date")),
EndDate: strings.TrimSpace(c.Query("end_date")),
FilterBy: strings.TrimSpace(c.Query("filter_by")),
PoDate: strings.TrimSpace(c.Query("po_date")),
PoDateFrom: strings.TrimSpace(c.Query("po_date_from")),
PoDateTo: strings.TrimSpace(c.Query("po_date_to")),
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")),
SupplierID: uint(c.QueryInt("supplier_id", 0)),
AreaID: uint(c.QueryInt("area_id", 0)),
LocationID: uint(c.QueryInt("location_id", 0)),
@@ -2,6 +2,7 @@ package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
@@ -42,13 +43,15 @@ func buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) {
}
}
grandTotals := buildPurchaseGrandTotalMap(purchases)
if err := setPurchaseExportColumns(file, purchaseExportSheetName); err != nil {
return nil, err
}
if err := setPurchaseExportHeaders(file, purchaseExportSheetName); err != nil {
return nil, err
}
if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases); err != nil {
if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases, grandTotals); err != nil {
return nil, err
}
if err := file.SetPanes(purchaseExportSheetName, &excelize.Panes{
@@ -77,17 +80,9 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
"F": 22,
"G": 22,
"H": 32,
"I": 10,
"J": 12,
"K": 16,
"L": 16,
"M": 22,
"N": 12,
"O": 16,
"P": 16,
"Q": 18,
"R": 18,
"S": 24,
"I": 18,
"J": 18,
"K": 24,
}
for col, width := range columnWidths {
@@ -104,25 +99,17 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
headers := []string{
"PR Number", // A
"PO Number", // B
"Tanggal PO", // C
"Tanggal Terima", // D
"Supplier", // E
"Lokasi", // F
"Gudang", // G
"Product", // H
"Qty", // I
"Satuan", // J
"Price", // K
"Total Produk", // L
"Vendor Ekspedisi",// M
"Qty Ekspedisi", // N
"Price Ekspedisi", // O
"Total Ekspedisi", // P
"Grand Total All", // Q
"Status", // R
"Notes", // S
"PR Number",
"PO Number",
"Tanggal PO",
"Tanggal Terima",
"Supplier",
"Lokasi",
"Gudang",
"Product",
"Status",
"Grand Total",
"Notes",
}
for i, header := range headers {
@@ -150,36 +137,34 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
return err
}
return file.SetCellStyle(sheet, "A1", "S1", headerStyle)
return file.SetCellStyle(sheet, "A1", "K1", headerStyle)
}
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase) error {
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase, grandTotals map[uint]float64) error {
if len(purchases) == 0 {
return nil
}
var sumL, sumP, sumQ float64
rowIdx := 2
for p := range purchases {
purchase := &purchases[p]
total := grandTotals[purchase.Id]
if len(purchase.Items) == 0 {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, &sumL, &sumP, &sumQ); err != nil {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, total); err != nil {
return err
}
rowIdx++
continue
}
for it := range purchase.Items {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], &sumL, &sumP, &sumQ); err != nil {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], total); err != nil {
return err
}
rowIdx++
}
}
lastDataRow := rowIdx - 1
lastRow := rowIdx - 1
dataStyle, err := file.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "left",
@@ -196,7 +181,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A2", "S"+strconv.Itoa(lastDataRow), dataStyle); err != nil {
if err := file.SetCellStyle(sheet, "A2", "K"+strconv.Itoa(lastRow), dataStyle); err != nil {
return err
}
@@ -215,17 +200,14 @@ func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "K2", "Q"+strconv.Itoa(lastDataRow), moneyStyle); err != nil {
return err
}
return addPurchaseExportSumRow(file, sheet, rowIdx, sumL, sumP, sumQ)
return file.SetCellStyle(sheet, "J2", "J"+strconv.Itoa(lastRow), moneyStyle)
}
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, sumL, sumP, sumQ *float64) error {
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, grandTotal float64) error {
row := strconv.Itoa(rowIdx)
// Purchase-level columns (repeat for every item row of the same purchase)
// Purchase-level columns (repeat across rows of the same purchase)
if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(purchase.PrNumber)); err != nil {
return err
}
@@ -238,40 +220,26 @@ func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purch
if err := file.SetCellValue(sheet, "E"+row, safePurchaseExportEntitySupplierName(purchase)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "R"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
if err := file.SetCellValue(sheet, "I"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "S"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil {
if err := file.SetCellValue(sheet, "J"+row, formatPurchaseRupiah(grandTotal)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "K"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil {
return err
}
// Item-level columns
if item == nil {
for _, col := range []string{"D", "F", "G", "H", "J", "M"} {
for _, col := range []string{"D", "F", "G", "H"} {
if err := file.SetCellValue(sheet, col+row, "-"); err != nil {
return err
}
}
for _, col := range []string{"I", "K", "L", "N", "O", "P", "Q"} {
if err := file.SetCellValue(sheet, col+row, 0); err != nil {
return err
}
}
return nil
}
// Item-level columns
var expeditionQty, expeditionPrice, expeditionTotal float64
if item.ExpenseNonstock != nil {
expeditionQty = item.ExpenseNonstock.Qty
expeditionPrice = item.ExpenseNonstock.Price
expeditionTotal = expeditionQty * expeditionPrice
}
itemGrandTotal := item.TotalPrice + expeditionTotal
*sumL += item.TotalPrice
*sumP += expeditionTotal
*sumQ += itemGrandTotal
if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil {
return err
}
@@ -284,96 +252,20 @@ func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purch
if err := file.SetCellValue(sheet, "H"+row, safePurchaseItemProductName(item)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "I"+row, item.TotalQty); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+row, safePurchaseItemUomName(item)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "K"+row, item.Price); err != nil {
return err
}
if err := file.SetCellValue(sheet, "L"+row, item.TotalPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "M"+row, safePurchaseItemExpeditionVendorName(item)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+row, expeditionQty); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+row, expeditionPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+row, expeditionTotal); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q"+row, itemGrandTotal); err != nil {
return err
}
return nil
}
func addPurchaseExportSumRow(file *excelize.File, sheet string, rowIdx int, sumL, sumP, sumQ float64) error {
row := strconv.Itoa(rowIdx)
sumStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "1F2937"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}},
Alignment: &excelize.Alignment{
Horizontal: "left",
Vertical: "center",
},
Border: []excelize.Border{
{Type: "left", Color: "D1D5DB", Style: 1},
{Type: "top", Color: "D1D5DB", Style: 2},
{Type: "bottom", Color: "D1D5DB", Style: 1},
{Type: "right", Color: "D1D5DB", Style: 1},
},
})
if err != nil {
return err
func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 {
result := make(map[uint]float64, len(items))
for i := range items {
total := 0.0
for j := range items[i].Items {
total += items[i].Items[j].TotalPrice
}
result[items[i].Id] = total
}
sumMoneyStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "1F2937"},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}},
Alignment: &excelize.Alignment{
Horizontal: "right",
Vertical: "center",
},
Border: []excelize.Border{
{Type: "left", Color: "D1D5DB", Style: 1},
{Type: "top", Color: "D1D5DB", Style: 2},
{Type: "bottom", Color: "D1D5DB", Style: 1},
{Type: "right", Color: "D1D5DB", Style: 1},
},
})
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A"+row, "S"+row, sumStyle); err != nil {
return err
}
if err := file.SetCellStyle(sheet, "L"+row, "L"+row, sumMoneyStyle); err != nil {
return err
}
if err := file.SetCellStyle(sheet, "P"+row, "Q"+row, sumMoneyStyle); err != nil {
return err
}
if err := file.SetCellValue(sheet, "A"+row, "TOTAL"); err != nil {
return err
}
if err := file.SetCellValue(sheet, "L"+row, sumL); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+row, sumP); err != nil {
return err
}
return file.SetCellValue(sheet, "Q"+row, sumQ)
return result
}
func safePurchaseExportEntitySupplierName(purchase *entity.Purchase) string {
@@ -404,24 +296,6 @@ func safePurchaseItemProductName(item *entity.PurchaseItem) string {
return safePurchaseExportText(item.Product.Name)
}
func safePurchaseItemUomName(item *entity.PurchaseItem) string {
if item.Product == nil || item.Product.Uom.Id == 0 {
return "-"
}
return safePurchaseExportText(item.Product.Uom.Name)
}
func safePurchaseItemExpeditionVendorName(item *entity.PurchaseItem) string {
if item.ExpenseNonstock == nil || item.ExpenseNonstock.Expense == nil {
return "-"
}
exp := item.ExpenseNonstock.Expense
if exp.Supplier == nil || exp.Supplier.Id == 0 {
return "-"
}
return safePurchaseExportText(exp.Supplier.Name)
}
func formatPurchaseExportEntityStatus(purchase *entity.Purchase) string {
if purchase.LatestApproval == nil {
return "-"
@@ -464,3 +338,37 @@ func safePurchaseExportText(value string) string {
return trimmed
}
func formatPurchaseRupiah(value float64) string {
if math.IsNaN(value) || math.IsInf(value, 0) {
return "Rp 0"
}
rounded := int64(math.Round(value))
sign := ""
if rounded < 0 {
sign = "-"
rounded = -rounded
}
raw := strconv.FormatInt(rounded, 10)
if raw == "" {
raw = "0"
}
var grouped strings.Builder
rem := len(raw) % 3
if rem > 0 {
grouped.WriteString(raw[:rem])
if len(raw) > rem {
grouped.WriteString(".")
}
}
for i := rem; i < len(raw); i += 3 {
grouped.WriteString(raw[i : i+3])
if i+3 < len(raw) {
grouped.WriteString(".")
}
}
return "Rp " + sign + grouped.String()
}
@@ -22,8 +22,9 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
nil,
"catatan",
[]entity.PurchaseItem{
buildPurchaseItemForExportTest(11, "Pakan Starter", 500, 2, 1000000, "Location A", "kg"),
buildPurchaseItemForExportTest(12, "Vitamin A", 350, 1, 350000, "Location B", "botol"),
buildPurchaseItemForExportTest(11, "Pakan Starter", 1000000, "Location A"),
buildPurchaseItemForExportTest(12, "Vitamin A", 350000, "Location B"),
buildPurchaseItemForExportTest(11, "Pakan Starter", 0, ""),
},
),
buildPurchaseForExportTest(
@@ -36,7 +37,7 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
ptrApprovalAction(entity.ApprovalActionRejected),
"",
[]entity.PurchaseItem{
buildPurchaseItemForExportTest(21, "Obat X", 75000, 1, 75000, "", ""),
buildPurchaseItemForExportTest(21, "Obat X", 75000, ""),
},
),
})
@@ -50,27 +51,16 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
}
defer file.Close()
// Verify all 19 headers
expectedHeaders := map[string]string{
"A1": "PR Number",
"B1": "PO Number",
"C1": "Tanggal PO",
"D1": "Tanggal Terima",
"E1": "Supplier",
"F1": "Lokasi",
"G1": "Gudang",
"H1": "Product",
"I1": "Qty",
"J1": "Satuan",
"K1": "Price",
"L1": "Total Produk",
"M1": "Vendor Ekspedisi",
"N1": "Qty Ekspedisi",
"O1": "Price Ekspedisi",
"P1": "Total Ekspedisi",
"Q1": "Grand Total All",
"R1": "Status",
"S1": "Notes",
"D1": "Supplier",
"E1": "Lokasi",
"F1": "Status",
"G1": "Grand Total",
"H1": "Products",
"I1": "Notes",
}
for cell, expected := range expectedHeaders {
got, err := file.GetCellValue(purchaseExportSheetName, cell)
@@ -82,46 +72,24 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
}
}
// Row 2: Purchase 1, Item 1 (Pakan Starter)
assertPurchaseCellEquals(t, file, "A2", "PR-00011")
assertPurchaseCellEquals(t, file, "B2", "PO-00011")
assertPurchaseCellEquals(t, file, "C2", "22-04-2026")
assertPurchaseCellEquals(t, file, "E2", "Supplier A")
assertPurchaseCellEquals(t, file, "F2", "Location A")
assertPurchaseCellEquals(t, file, "H2", "Pakan Starter")
assertPurchaseCellEquals(t, file, "J2", "kg")
assertPurchaseCellEquals(t, file, "K2", "500")
assertPurchaseCellEquals(t, file, "L2", "1000000")
assertPurchaseCellEquals(t, file, "M2", "-")
assertPurchaseCellEquals(t, file, "P2", "0")
assertPurchaseCellEquals(t, file, "Q2", "1000000")
assertPurchaseCellEquals(t, file, "R2", "Manager Purchase")
assertPurchaseCellEquals(t, file, "S2", "catatan")
assertPurchaseCellEquals(t, file, "D2", "Supplier A")
assertPurchaseCellEquals(t, file, "E2", "Location A")
assertPurchaseCellEquals(t, file, "F2", "Manager Purchase")
assertPurchaseCellEquals(t, file, "G2", "Rp 1.350.000")
assertPurchaseCellEquals(t, file, "H2", "Pakan Starter, Vitamin A")
assertPurchaseCellEquals(t, file, "I2", "catatan")
// Row 3: Purchase 1, Item 2 (Vitamin A)
assertPurchaseCellEquals(t, file, "A3", "PR-00011")
assertPurchaseCellEquals(t, file, "H3", "Vitamin A")
assertPurchaseCellEquals(t, file, "J3", "botol")
assertPurchaseCellEquals(t, file, "L3", "350000")
assertPurchaseCellEquals(t, file, "Q3", "350000")
// Row 4: Purchase 2, Item 1 (Obat X) — no location, rejected
assertPurchaseCellEquals(t, file, "A4", "PR-00012")
assertPurchaseCellEquals(t, file, "B4", "-")
assertPurchaseCellEquals(t, file, "C4", "-")
assertPurchaseCellEquals(t, file, "F4", "-")
assertPurchaseCellEquals(t, file, "H4", "Obat X")
assertPurchaseCellEquals(t, file, "J4", "-")
assertPurchaseCellEquals(t, file, "L4", "75000")
assertPurchaseCellEquals(t, file, "Q4", "75000")
assertPurchaseCellEquals(t, file, "R4", "Ditolak")
assertPurchaseCellEquals(t, file, "S4", "-")
// Row 5: SUM row — total produk=1425000, ekspedisi=0, grand total all=1425000
assertPurchaseCellEquals(t, file, "A5", "TOTAL")
assertPurchaseCellEquals(t, file, "L5", "1425000")
assertPurchaseCellEquals(t, file, "P5", "0")
assertPurchaseCellEquals(t, file, "Q5", "1425000")
assertPurchaseCellEquals(t, file, "A3", "PR-00012")
assertPurchaseCellEquals(t, file, "B3", "-")
assertPurchaseCellEquals(t, file, "C3", "-")
assertPurchaseCellEquals(t, file, "E3", "-")
assertPurchaseCellEquals(t, file, "F3", "Ditolak")
assertPurchaseCellEquals(t, file, "G3", "Rp 75.000")
assertPurchaseCellEquals(t, file, "H3", "Obat X")
assertPurchaseCellEquals(t, file, "I3", "-")
}
func assertPurchaseCellEquals(t *testing.T, file *excelize.File, cell, expected string) {
@@ -176,20 +144,13 @@ func buildPurchaseForExportTest(
}
}
func buildPurchaseItemForExportTest(productID uint, productName string, price, totalQty, totalPrice float64, locationName, uomName string) entity.PurchaseItem {
uomID := uint(0)
if uomName != "" {
uomID = productID + 2000
}
func buildPurchaseItemForExportTest(productID uint, productName string, totalPrice float64, locationName string) entity.PurchaseItem {
item := entity.PurchaseItem{
ProductId: productID,
Price: price,
TotalQty: totalQty,
TotalPrice: totalPrice,
Product: &entity.Product{
Id: productID,
Name: productName,
Uom: entity.Uom{Id: uomID, Name: uomName},
},
}
+10 -26
View File
@@ -32,15 +32,12 @@ type PurchaseListDTO struct {
RequesterName string `json:"requester_name"`
PoExpedition []PoExpeditionDTO `json:"po_expedition"`
Items []PurchaseItemDTO `json:"items"`
Products []productDTO.ProductRelationDTO `json:"products"`
Location *locationDTO.LocationRelationDTO `json:"location"`
Area *areaDTO.AreaRelationDTO `json:"area"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
ProductsTotal float64 `json:"products_total"`
ExpeditionTotal float64 `json:"expedition_total"`
GrandTotalAll float64 `json:"grand_total_all"`
Products []productDTO.ProductRelationDTO `json:"products"`
Location *locationDTO.LocationRelationDTO `json:"location"`
Area *areaDTO.AreaRelationDTO `json:"area"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type PurchaseDetailDTO struct {
@@ -72,8 +69,6 @@ type PurchaseItemDTO struct {
VehicleNumber *string `json:"vehicle_number"`
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
ExpeditionQty float64 `json:"expedition_qty"`
ExpeditionTotal float64 `json:"expedition_total"`
HasChickin bool `json:"has_chickin"`
}
@@ -132,8 +127,6 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
if item.ExpenseNonstock != nil {
priceCopy := item.ExpenseNonstock.Price
dto.TransportPerItem = &priceCopy
dto.ExpeditionQty = item.ExpenseNonstock.Qty
dto.ExpeditionTotal = item.ExpenseNonstock.Qty * item.ExpenseNonstock.Price
if item.ExpenseNonstock.Expense != nil {
exp := item.ExpenseNonstock.Expense
@@ -180,21 +173,15 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
}
var (
poExpedition = make([]PoExpeditionDTO, 0)
location *locationDTO.LocationRelationDTO
area *areaDTO.AreaRelationDTO
receivedDate *time.Time
productsTotal float64
expeditionTotal float64
poExpedition = make([]PoExpeditionDTO, 0)
location *locationDTO.LocationRelationDTO
area *areaDTO.AreaRelationDTO
receivedDate *time.Time
)
productMap := make(map[uint]productDTO.ProductRelationDTO)
expeditionRefSet := make(map[uint64]struct{})
for i := range p.Items {
item := p.Items[i]
productsTotal += item.TotalPrice
if item.ExpenseNonstock != nil {
expeditionTotal += item.ExpenseNonstock.Qty * item.ExpenseNonstock.Price
}
if item.Product != nil && item.Product.Id != 0 {
if _, exists := productMap[item.Product.Id]; !exists {
productMap[item.Product.Id] = productDTO.ToProductRelationDTO(*item.Product)
@@ -248,9 +235,6 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval,
ProductsTotal: productsTotal,
ExpeditionTotal: expeditionTotal,
GrandTotalAll: productsTotal + expeditionTotal,
}
}
@@ -145,16 +145,33 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
offset := (params.Page - 1) * params.Limit
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
if err != nil {
return nil, 0, utils.BadRequest(err.Error())
}
productCategoryIDs, err := parseUintCSVFilter(params.ProductCategoryID, "product_category_id")
if err != nil {
return nil, 0, utils.BadRequest(err.Error())
}
dateStart, dateEnd, err := parsePurchaseDateRangeForQuery(params.StartDate, params.EndDate, "date")
if err != nil {
return nil, 0, utils.BadRequest(err.Error())
var poDateStart *time.Time
var poDateEnd *time.Time
if strings.TrimSpace(params.PoDate) != "" {
poDate, parseErr := utils.ParseDateString(strings.TrimSpace(params.PoDate))
if parseErr != nil {
return nil, 0, utils.BadRequest("po_date must use format YYYY-MM-DD")
}
poDateStart = &poDate
poDateEndValue := poDate.AddDate(0, 0, 1)
poDateEnd = &poDateEndValue
} else {
poDateStart, poDateEnd, err = parsePoDateRangeForQuery(params.PoDateFrom, params.PoDateTo)
if err != nil {
return nil, 0, utils.BadRequest(err.Error())
}
}
filterBy := strings.TrimSpace(params.FilterBy)
search := strings.ToLower(strings.TrimSpace(params.Search))
approvalStatuses := parseStringCSVFilter(params.ApprovalStatus)
@@ -170,41 +187,23 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
db = db.Where("supplier_id = ?", params.SupplierID)
}
switch filterBy {
case "po_date":
if dateStart != nil {
db = db.Where("purchases.po_date >= ?", *dateStart)
}
if dateEnd != nil {
db = db.Where("purchases.po_date < ?", *dateEnd)
}
case "due_date":
if dateStart != nil {
db = db.Where("purchases.due_date >= ?", *dateStart)
}
if dateEnd != nil {
db = db.Where("purchases.due_date < ?", *dateEnd)
}
case "received_date":
if dateStart != nil {
db = db.Where(
`EXISTS (SELECT 1 FROM purchase_items pi WHERE pi.purchase_id = purchases.id AND pi.received_date >= ?)`,
*dateStart,
)
}
if dateEnd != nil {
db = db.Where(
`EXISTS (SELECT 1 FROM purchase_items pi WHERE pi.purchase_id = purchases.id AND pi.received_date < ?)`,
*dateEnd,
)
}
default:
if dateStart != nil {
db = db.Where("purchases.created_at >= ?", *dateStart)
}
if dateEnd != nil {
db = db.Where("purchases.created_at < ?", *dateEnd)
}
if createdFrom != nil {
db = db.Where("created_at >= ?", *createdFrom)
}
if createdTo != nil {
db = db.Where("created_at < ?", *createdTo)
}
if poDateStart != nil {
db = db.Where("purchases.po_date >= ?", *poDateStart)
}
if poDateStart != nil {
db = db.Where("purchases.po_date >= ?", *poDateStart)
}
if poDateEnd != nil {
db = db.Where("purchases.po_date < ?", *poDateEnd)
}
if scope.Restrict {
@@ -2239,29 +2238,30 @@ func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []ent
return nil
}
func parsePurchaseDateRangeForQuery(fromStr, toStr, fieldName string) (*time.Time, *time.Time, error) {
func parsePoDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, error) {
var fromPtr *time.Time
var toPtr *time.Time
if strings.TrimSpace(fromStr) != "" {
parsed, err := utils.ParseDateString(strings.TrimSpace(fromStr))
if err != nil {
return nil, nil, errors.New(fieldName + "_from must use format YYYY-MM-DD")
return nil, nil, errors.New("po_date_from must use format YYYY-MM-DD")
}
fromPtr = &parsed
fromValue := parsed
fromPtr = &fromValue
}
if strings.TrimSpace(toStr) != "" {
parsed, err := utils.ParseDateString(strings.TrimSpace(toStr))
if err != nil {
return nil, nil, errors.New(fieldName + "_to must use format YYYY-MM-DD")
return nil, nil, errors.New("po_date_to must use format YYYY-MM-DD")
}
nextDay := parsed.AddDate(0, 0, 1)
toPtr = &nextDay
}
if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) {
return nil, nil, errors.New(fieldName + "_from must be earlier than " + fieldName + "_to")
return nil, nil, errors.New("po_date_from must be earlier than po_date_to")
}
return fromPtr, toPtr, nil
@@ -75,10 +75,12 @@ type Query struct {
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
ProductCategoryID string `query:"product_category_id" validate:"omitempty,max=500"`
ApprovalStatus string `query:"approval_status" validate:"omitempty,max=500"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=po_date due_date received_date created_at"`
PoDate string `query:"po_date" validate:"omitempty,datetime=2006-01-02"`
PoDateFrom string `query:"po_date_from" validate:"omitempty,datetime=2006-01-02"`
PoDateTo string `query:"po_date_to" validate:"omitempty,datetime=2006-01-02"`
Search string `query:"search" validate:"omitempty,max=100"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=po_expedition supplier requester_name products location po_date received_date due_date status created_at po_number"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc ASC DESC"`
}
@@ -392,13 +392,6 @@ func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error {
return err
}
if isDebtSupplierExcelExportRequest(ctx) {
return exportDebtSupplierExcel(ctx, result)
}
if isDebtSupplierExcelAllExportRequest(ctx) {
return exportDebtSupplierExcelAll(ctx, result)
}
supplierIDs = query.SupplierIDs
if supplierIDs == nil {
supplierIDs = []int64{}
@@ -485,13 +478,6 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
return err
}
if isCustomerPaymentExcelExportRequest(ctx) {
return exportCustomerPaymentExcel(ctx, result)
}
if isCustomerPaymentExcelAllExportRequest(ctx) {
return exportCustomerPaymentExcelAll(ctx, result)
}
// If single customer mode (only 1 customer ID), return without pagination
if len(customerIDs) == 1 {
return ctx.Status(fiber.StatusOK).
@@ -519,83 +505,6 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
})
}
type BalanceMonitoringResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta response.Meta `json:"meta"`
Data []dto.BalanceMonitoringRowDTO `json:"data"`
Totals dto.BalanceMonitoringTotalsDTO `json:"totals"`
}
func (c *RepportController) GetBalanceMonitoring(ctx *fiber.Ctx) error {
customerIDs, err := parseUintCSV(ctx.Query("customer_ids"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "customer_ids must be comma separated positive integers")
}
salesIDs, err := parseUintCSV(ctx.Query("sales_ids"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "sales_ids must be comma separated positive integers")
}
query := &validation.BalanceMonitoringQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
CustomerIDs: customerIDs,
SalesIDs: salesIDs,
FilterBy: strings.ToLower(ctx.Query("filter_by", "")),
SortBy: ctx.Query("sort_by", ""),
SortOrder: ctx.Query("sort_order", ""),
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
}
result, totals, totalResults, err := c.RepportService.GetBalanceMonitoring(ctx, query)
if err != nil {
return err
}
limit := query.Limit
if limit < 1 {
limit = 10
}
return ctx.Status(fiber.StatusOK).JSON(BalanceMonitoringResponse{
Code: fiber.StatusOK,
Status: "success",
Message: "Get balance monitoring report successfully",
Meta: response.Meta{
Page: query.Page,
Limit: limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(limit))),
TotalResults: totalResults,
},
Data: result,
Totals: totals,
})
}
func parseUintCSV(raw string) ([]uint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
parts := strings.Split(raw, ",")
result := make([]uint, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, err := strconv.ParseUint(part, 10, 32)
if err != nil || id == 0 {
return nil, fmt.Errorf("invalid id: %s", part)
}
result = append(result, uint(id))
}
return result, nil
}
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
idParam := ctx.Params("idProjectFlockKandang")
if idParam == "" {
@@ -1,576 +0,0 @@
package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
)
func isCustomerPaymentExcelExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
}
func isCustomerPaymentExcelAllExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all")
}
func exportCustomerPaymentExcel(c *fiber.Ctx, items []dto.CustomerPaymentReportItem) error {
content, err := buildCustomerPaymentWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-kontrol-pembayaran-customer-%s.xlsx", time.Now().Format("2006-01-02-1504"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(content)
}
func exportCustomerPaymentExcelAll(c *fiber.Ctx, items []dto.CustomerPaymentReportItem) error {
content, err := buildCustomerPaymentAllWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-kontrol-pembayaran-customer-all-%s.xlsx", time.Now().Format("2006-01-02-1504"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(content)
}
func buildCustomerPaymentWorkbook(items []dto.CustomerPaymentReportItem) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if len(items) == 0 {
if err := writeCustomerPaymentSheet(file, defaultSheet, dto.CustomerPaymentReportItem{}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
for idx, item := range items {
sheetName := sanitizeCustomerPaymentSheetName(customerPaymentName(item))
if sheetName == "" {
sheetName = fmt.Sprintf("Customer %d", idx+1)
}
if idx == 0 {
if defaultSheet != sheetName {
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
return nil, err
}
}
} else {
if _, err := file.NewSheet(sheetName); err != nil {
return nil, err
}
}
if err := writeCustomerPaymentSheet(file, sheetName, item); err != nil {
return nil, err
}
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func buildCustomerPaymentAllWorkbook(items []dto.CustomerPaymentReportItem) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
const sheet = "Kontrol Pembayaran Customer"
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if defaultSheet != sheet {
if err := file.SetSheetName(defaultSheet, sheet); err != nil {
return nil, err
}
}
if err := setCustomerPaymentAllColumns(file, sheet); err != nil {
return nil, err
}
if err := setCustomerPaymentAllHeaders(file, sheet); err != nil {
return nil, err
}
if err := writeCustomerPaymentAllRows(file, sheet, items); err != nil {
return nil, err
}
if err := file.SetPanes(sheet, &excelize.Panes{
Freeze: true,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var cpSheetHeaders = []string{
"No",
"Tanggal DO/Bayar",
"Tanggal Realisasi",
"Aging",
"Referensi",
"Nomor Polisi",
"Ekor/Qty",
"Berat (Kg)",
"AVG",
"Harga/Unit (Rp)",
"Harga Akhir (Rp)",
"Total (Rp)",
"Pembayaran (Rp)",
"Saldo Piutang (Rp)",
"Keterangan",
"Pengambilan",
"Sales/Marketing",
}
var cpAllSheetHeaders = append([]string{"Customer"}, cpSheetHeaders...)
var cpSheetColumnWidths = map[string]float64{
"A": 5,
"B": 15,
"C": 12,
"D": 8,
"E": 12,
"F": 15,
"G": 10,
"H": 12,
"I": 10,
"J": 15,
"K": 15,
"L": 15,
"M": 15,
"N": 15,
"O": 20,
"P": 15,
"Q": 20,
}
var cpAllSheetColumnWidths = map[string]float64{
"A": 22,
"B": 6,
"C": 15,
"D": 15,
"E": 8,
"F": 12,
"G": 15,
"H": 10,
"I": 12,
"J": 10,
"K": 15,
"L": 15,
"M": 15,
"N": 15,
"O": 15,
"P": 20,
"Q": 15,
"R": 20,
}
func writeCustomerPaymentSheet(file *excelize.File, sheet string, item dto.CustomerPaymentReportItem) error {
for col, width := range cpSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
// Row 1: headers
for i, h := range cpSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
redStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "FF0000"},
})
if err != nil {
return err
}
// Row 2: saldo awal
if err := file.SetCellValue(sheet, "N2", item.InitialBalance); err != nil {
return err
}
if item.InitialBalance < 0 {
if err := file.SetCellStyle(sheet, "N2", "N2", redStyle); err != nil {
return err
}
}
// Rows 3+: data rows
for i, row := range item.Rows {
rowNum := i + 3
rowStr := fmt.Sprintf("%d", rowNum)
cells := customerPaymentRowCells(row, i+1)
for colIdx, val := range cells {
col, _ := excelize.ColumnNumberToName(colIdx + 1)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if row.AccountsReceivable < 0 {
if err := file.SetCellStyle(sheet, "N"+rowStr, "N"+rowStr, redStyle); err != nil {
return err
}
}
}
// Total row
totalRowNum := len(item.Rows) + 3
totalRowStr := fmt.Sprintf("%d", totalRowNum)
totalCells := map[string]interface{}{
"A": "Total",
"G": formatCPIDInteger(item.Summary.TotalQty),
"H": formatCPIDInteger(item.Summary.TotalWeight),
"K": item.Summary.TotalFinalAmount,
"L": item.Summary.TotalGrandAmount,
"M": item.Summary.TotalPayment,
"N": item.Summary.TotalAccountsReceivable,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
return err
}
}
if item.Summary.TotalAccountsReceivable < 0 {
if err := file.SetCellStyle(sheet, "N"+totalRowStr, "N"+totalRowStr, redStyle); err != nil {
return err
}
}
return nil
}
func setCustomerPaymentAllColumns(file *excelize.File, sheet string) error {
for col, width := range cpAllSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
return file.SetRowHeight(sheet, 1, 24)
}
func setCustomerPaymentAllHeaders(file *excelize.File, sheet string) error {
borderStyle := []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
}
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
WrapText: true,
},
Border: borderStyle,
})
if err != nil {
return err
}
for i, h := range cpAllSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
lastCol, _ := excelize.ColumnNumberToName(len(cpAllSheetHeaders))
return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle)
}
func writeCustomerPaymentAllRows(file *excelize.File, sheet string, items []dto.CustomerPaymentReportItem) error {
borderStyle := []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
}
dataStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
totalStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
redDataStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "FF0000", Family: "Arial", Size: 10},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
redTotalStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FF0000", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
lastHeaderCol, _ := excelize.ColumnNumberToName(len(cpAllSheetHeaders))
currentRow := 2
for _, item := range items {
name := customerPaymentName(item)
// Saldo awal row
saldoStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+saldoStr, name); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+saldoStr, item.InitialBalance); err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A"+saldoStr, lastHeaderCol+saldoStr, dataStyle); err != nil {
return err
}
if item.InitialBalance < 0 {
if err := file.SetCellStyle(sheet, "O"+saldoStr, "O"+saldoStr, redDataStyle); err != nil {
return err
}
}
currentRow++
// Data rows
for seq, row := range item.Rows {
rowStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+rowStr, name); err != nil {
return err
}
cells := customerPaymentRowCells(row, seq+1)
for colIdx, val := range cells {
col, _ := excelize.ColumnNumberToName(colIdx + 2)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+rowStr, lastHeaderCol+rowStr, dataStyle); err != nil {
return err
}
if row.AccountsReceivable < 0 {
if err := file.SetCellStyle(sheet, "O"+rowStr, "O"+rowStr, redDataStyle); err != nil {
return err
}
}
currentRow++
}
// Total row
totalStr := fmt.Sprintf("%d", currentRow)
totalCells := map[string]interface{}{
"A": name,
"B": "Total",
"H": formatCPIDInteger(item.Summary.TotalQty),
"I": formatCPIDInteger(item.Summary.TotalWeight),
"L": item.Summary.TotalFinalAmount,
"M": item.Summary.TotalGrandAmount,
"N": item.Summary.TotalPayment,
"O": item.Summary.TotalAccountsReceivable,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+totalStr, lastHeaderCol+totalStr, totalStyle); err != nil {
return err
}
if item.Summary.TotalAccountsReceivable < 0 {
if err := file.SetCellStyle(sheet, "O"+totalStr, "O"+totalStr, redTotalStyle); err != nil {
return err
}
}
currentRow++
// Empty separator row
currentRow++
}
return nil
}
// customerPaymentRowCells returns 17 cell values for cols A..Q.
func customerPaymentRowCells(row dto.CustomerPaymentReportRow, seq int) []interface{} {
return []interface{}{
seq,
formatCPDate(row.TransDate),
formatCPOptionalDate(row.DeliveryDate),
formatCPAging(row.AgingDay),
safeCPText(row.Reference),
joinCPStrings(row.VehicleNumbers),
formatCPIDInteger(row.Qty),
formatCPIDInteger(row.Weight),
formatCPAvg(row.AverageWeight),
row.UnitPrice,
row.FinalPrice,
row.TotalPrice,
row.PaymentAmount,
row.AccountsReceivable,
safeCPText(row.Status),
joinCPStrings(row.PickupInfo),
safeCPText(row.SalesPerson),
}
}
func customerPaymentName(item dto.CustomerPaymentReportItem) string {
name := strings.TrimSpace(item.Customer.Name)
if name == "" {
return "Customer"
}
return name
}
func sanitizeCustomerPaymentSheetName(name string) string {
replacer := strings.NewReplacer(
":", " ", "\\", " ", "/", " ",
"?", " ", "*", " ", "[", " ", "]", " ",
)
sanitized := strings.TrimSpace(replacer.Replace(name))
if sanitized == "" {
return "Sheet"
}
runes := []rune(sanitized)
if len(runes) > 31 {
return string(runes[:31])
}
return sanitized
}
var cpIndonesianMonths = [12]string{
"Jan", "Feb", "Mar", "Apr", "Mei", "Jun",
"Jul", "Agu", "Sep", "Okt", "Nov", "Des",
}
func formatCPDate(t time.Time) string {
if t.IsZero() {
return "-"
}
loc, err := time.LoadLocation("Asia/Jakarta")
if err == nil {
t = t.In(loc)
}
return fmt.Sprintf("%02d %s %d", t.Day(), cpIndonesianMonths[t.Month()-1], t.Year())
}
func formatCPOptionalDate(t *time.Time) string {
if t == nil || t.IsZero() {
return "-"
}
return formatCPDate(*t)
}
func formatCPAging(v *int) string {
if v == nil {
return "-"
}
return strconv.Itoa(*v)
}
func formatCPIDInteger(v float64) string {
n := int64(math.Round(v))
if n == 0 {
return "0"
}
negative := n < 0
abs := n
if negative {
abs = -n
}
s := strconv.FormatInt(abs, 10)
// insert dots as thousand separators
var b strings.Builder
start := len(s) % 3
if start == 0 {
start = 3
}
b.WriteString(s[:start])
for i := start; i < len(s); i += 3 {
b.WriteByte('.')
b.WriteString(s[i : i+3])
}
if negative {
return "-" + b.String()
}
return b.String()
}
func formatCPAvg(v float64) string {
if v == 0 {
return "0"
}
s := strconv.FormatFloat(v, 'f', 2, 64)
return strings.ReplaceAll(s, ".", ",")
}
func safeCPText(s string) string {
t := strings.TrimSpace(s)
if t == "" {
return "-"
}
return t
}
func joinCPStrings(ss []string) string {
var parts []string
for _, s := range ss {
s = strings.TrimSpace(s)
if s != "" {
parts = append(parts, s)
}
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, "\n")
}
@@ -1,452 +0,0 @@
package controller
import (
"fmt"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
)
func isDebtSupplierExcelExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
}
func isDebtSupplierExcelAllExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all")
}
func exportDebtSupplierExcel(c *fiber.Ctx, items []dto.DebtSupplierDTO) error {
content, err := buildDebtSupplierWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-hutang-supplier-%s.xlsx", time.Now().Format("2006-01-02-1504"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(content)
}
func exportDebtSupplierExcelAll(c *fiber.Ctx, items []dto.DebtSupplierDTO) error {
content, err := buildDebtSupplierAllWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-hutang-supplier-all-%s.xlsx", time.Now().Format("2006-01-02-1504"))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
return c.Status(fiber.StatusOK).Send(content)
}
// buildDebtSupplierWorkbook creates a workbook with one sheet per supplier.
func buildDebtSupplierWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if len(items) == 0 {
if err := writeDebtSupplierSheet(file, defaultSheet, dto.DebtSupplierDTO{}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
for idx, item := range items {
sheetName := sanitizeDebtSupplierSheetName(debtSupplierName(item))
if sheetName == "" {
sheetName = fmt.Sprintf("Supplier %d", idx+1)
}
if idx == 0 {
if defaultSheet != sheetName {
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
return nil, err
}
}
} else {
if _, err := file.NewSheet(sheetName); err != nil {
return nil, err
}
}
if err := writeDebtSupplierSheet(file, sheetName, item); err != nil {
return nil, err
}
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// buildDebtSupplierAllWorkbook creates a single-sheet workbook with purchase-supplier styling.
func buildDebtSupplierAllWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
const sheet = "Rekap Hutang Supplier"
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if defaultSheet != sheet {
if err := file.SetSheetName(defaultSheet, sheet); err != nil {
return nil, err
}
}
if err := setDebtSupplierAllColumns(file, sheet); err != nil {
return nil, err
}
if err := setDebtSupplierAllHeaders(file, sheet); err != nil {
return nil, err
}
if err := writeDebtSupplierAllRows(file, sheet, items); err != nil {
return nil, err
}
if err := file.SetPanes(sheet, &excelize.Panes{
Freeze: true,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var debtSupplierSheetHeaders = []string{
"No",
"Nomor PR",
"Nomor PO",
"Tanggal Terima/Bayar",
"Tanggal PO",
"Aging (Hari)",
"Area",
"Gudang",
"Jatuh Tempo",
"Status Jatuh Tempo",
"Nominal Pembelian (Rp)",
"Pembayaran (Rp)",
"Sisa Saldo Hutang (Rp)",
"Status",
"Nomor Perjalanan",
}
var debtSupplierAllSheetHeaders = append([]string{"Supplier"}, debtSupplierSheetHeaders...)
var debtSupplierSheetColumnWidths = map[string]float64{
"A": 5,
"B": 14,
"C": 12,
"D": 20,
"E": 10,
"F": 12,
"G": 15,
"H": 20,
"I": 12,
"J": 20,
"K": 20,
"L": 15,
"M": 20,
"N": 12,
"O": 15,
}
var debtSupplierAllSheetColumnWidths = map[string]float64{
"A": 24,
"B": 6,
"C": 14,
"D": 14,
"E": 20,
"F": 12,
"G": 10,
"H": 16,
"I": 22,
"J": 12,
"K": 22,
"L": 20,
"M": 18,
"N": 22,
"O": 14,
"P": 18,
}
func writeDebtSupplierSheet(file *excelize.File, sheet string, item dto.DebtSupplierDTO) error {
for col, width := range debtSupplierSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
// Row 1: headers
for i, h := range debtSupplierSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
// Row 2: saldo awal
if err := file.SetCellValue(sheet, "M2", item.InitialBalance); err != nil {
return err
}
// Rows 3+: data
redStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "FF0000"},
})
if err != nil {
return err
}
for i, row := range item.Rows {
rowNum := i + 3
rowStr := fmt.Sprintf("%d", rowNum)
values := debtSupplierRowCells(row, i+1)
for colIdx, val := range values {
col, _ := excelize.ColumnNumberToName(colIdx + 1)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if row.DebtPrice < 0 {
if err := file.SetCellStyle(sheet, "M"+rowStr, "M"+rowStr, redStyle); err != nil {
return err
}
}
}
// Total row
totalRowNum := len(item.Rows) + 3
totalRowStr := fmt.Sprintf("%d", totalRowNum)
totalCells := map[string]interface{}{
"A": "Total",
"F": item.Total.Aging,
"K": item.Total.TotalPrice,
"L": item.Total.PaymentPrice,
"M": item.Total.DebtPrice,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
return err
}
}
if item.Total.DebtPrice < 0 {
if err := file.SetCellStyle(sheet, "M"+totalRowStr, "M"+totalRowStr, redStyle); err != nil {
return err
}
}
return nil
}
func setDebtSupplierAllColumns(file *excelize.File, sheet string) error {
for col, width := range debtSupplierAllSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
if err := file.SetRowHeight(sheet, 1, 24); err != nil {
return err
}
return nil
}
func setDebtSupplierAllHeaders(file *excelize.File, sheet string) error {
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
WrapText: true,
},
Border: []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
},
})
if err != nil {
return err
}
for i, h := range debtSupplierAllSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
lastCol, _ := excelize.ColumnNumberToName(len(debtSupplierAllSheetHeaders))
return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle)
}
func writeDebtSupplierAllRows(file *excelize.File, sheet string, items []dto.DebtSupplierDTO) error {
borderStyle := []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
}
dataStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
totalStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
lastHeaderCol, _ := excelize.ColumnNumberToName(len(debtSupplierAllSheetHeaders))
currentRow := 2
for _, item := range items {
supplierName := debtSupplierName(item)
// Saldo awal row
saldoRowStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+saldoRowStr, supplierName); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+saldoRowStr, item.InitialBalance); err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A"+saldoRowStr, lastHeaderCol+saldoRowStr, dataStyle); err != nil {
return err
}
currentRow++
// Data rows
for seq, row := range item.Rows {
rowStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+rowStr, supplierName); err != nil {
return err
}
values := debtSupplierRowCells(row, seq+1)
for colIdx, val := range values {
col, _ := excelize.ColumnNumberToName(colIdx + 2)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+rowStr, lastHeaderCol+rowStr, dataStyle); err != nil {
return err
}
currentRow++
}
// Total row
totalRowStr := fmt.Sprintf("%d", currentRow)
totalCells := map[string]interface{}{
"A": supplierName,
"B": "Total",
"L": item.Total.TotalPrice,
"M": item.Total.PaymentPrice,
"N": item.Total.DebtPrice,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+totalRowStr, lastHeaderCol+totalRowStr, totalStyle); err != nil {
return err
}
currentRow++
// Empty separator row
currentRow++
}
return nil
}
// debtSupplierRowCells returns cell values for one data row (columns: No, PR, PO, ReceivedDate, PoDate, Aging, Area, Warehouse, DueDate, DueStatus, TotalPrice, PaymentPrice, DebtPrice, Status, TravelNumber).
func debtSupplierRowCells(row dto.DebtSupplierRowDTO, seq int) []interface{} {
areaName := "-"
if row.Area != nil && strings.TrimSpace(row.Area.Name) != "" {
areaName = row.Area.Name
}
warehouseName := "-"
if row.Warehouse != nil && strings.TrimSpace(row.Warehouse.Name) != "" {
warehouseName = row.Warehouse.Name
}
return []interface{}{
seq,
safeDebtSupplierText(row.PrNumber),
safeDebtSupplierText(row.PoNumber),
safeDebtSupplierText(row.ReceivedDate),
safeDebtSupplierText(row.PoDate),
row.Aging,
areaName,
warehouseName,
safeDebtSupplierText(row.DueDate),
safeDebtSupplierText(row.DueStatus),
row.TotalPrice,
row.PaymentPrice,
row.DebtPrice,
safeDebtSupplierText(row.Status),
safeDebtSupplierText(row.TravelNumber),
}
}
func debtSupplierName(item dto.DebtSupplierDTO) string {
if item.Supplier != nil && strings.TrimSpace(item.Supplier.Name) != "" {
return item.Supplier.Name
}
return "Supplier"
}
func sanitizeDebtSupplierSheetName(name string) string {
replacer := strings.NewReplacer(
":", " ", "\\", " ", "/", " ",
"?", " ", "*", " ", "[", " ", "]", " ",
)
sanitized := strings.TrimSpace(replacer.Replace(name))
if sanitized == "" {
return "Sheet"
}
runes := []rune(sanitized)
if len(runes) > 31 {
return string(runes[:31])
}
return sanitized
}
func safeDebtSupplierText(s string) string {
t := strings.TrimSpace(s)
if t == "" {
return "-"
}
return t
}
@@ -2,6 +2,7 @@ package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
@@ -196,9 +197,9 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte
item.Qty,
item.AverageWeightKg,
item.TotalWeightKg,
item.SalesPricePerKg,
item.HppPricePerKg,
item.SalesAmount,
formatMarketingRupiah(item.SalesPricePerKg),
formatMarketingRupiah(item.HppPricePerKg),
formatMarketingRupiah(item.SalesAmount),
}
for colIdx, val := range values {
@@ -228,13 +229,13 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte
if err := file.SetCellValue(sheet, "N"+totalRow, summary.TotalWeightKg); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+totalRow, summary.AverageSalesPrice); err != nil {
if err := file.SetCellValue(sheet, "O"+totalRow, formatMarketingRupiah(summary.AverageSalesPrice)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+totalRow, summary.TotalHppPricePerKg); err != nil {
if err := file.SetCellValue(sheet, "P"+totalRow, formatMarketingRupiah(summary.TotalHppPricePerKg)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q"+totalRow, float64(summary.TotalSalesAmount)); err != nil {
if err := file.SetCellValue(sheet, "Q"+totalRow, formatMarketingRupiah(float64(summary.TotalSalesAmount))); err != nil {
return err
}
}
@@ -332,3 +333,30 @@ func safeMarketingExportText(value string) string {
return trimmed
}
// formatMarketingRupiah formats a float64 as Indonesian Rupiah string.
// e.g. 1000000 → "Rp 1.000.000"
func formatMarketingRupiah(value float64) string {
rounded := int64(math.Round(value))
negative := rounded < 0
abs := rounded
if negative {
abs = -rounded
}
numStr := strconv.FormatInt(abs, 10)
n := len(numStr)
var b strings.Builder
for i, c := range numStr {
if i > 0 && (n-i)%3 == 0 {
b.WriteByte('.')
}
b.WriteRune(c)
}
if negative {
return "Rp -" + b.String()
}
return "Rp " + b.String()
}
@@ -1,71 +0,0 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
)
type BalanceMonitoringAyamDTO struct {
Ekor float64 `json:"ekor"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringTelurDTO struct {
Butir float64 `json:"butir"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringTradingDTO struct {
Qty float64 `json:"qty"`
Kg float64 `json:"kg"`
Nominal float64 `json:"nominal"`
}
type BalanceMonitoringRowDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SaldoAwal float64 `json:"saldo_awal"`
PenjualanAyam BalanceMonitoringAyamDTO `json:"penjualan_ayam"`
PenjualanTelur BalanceMonitoringTelurDTO `json:"penjualan_telur"`
PenjualanTrading BalanceMonitoringTradingDTO `json:"penjualan_trading"`
Pembayaran float64 `json:"pembayaran"`
Aging int `json:"aging"`
AgingRataRata float64 `json:"aging_rata_rata"`
SaldoAkhir float64 `json:"saldo_akhir"`
}
type BalanceMonitoringTotalsDTO struct {
SaldoAwal float64 `json:"saldo_awal"`
PenjualanAyam BalanceMonitoringAyamDTO `json:"penjualan_ayam"`
PenjualanTelur BalanceMonitoringTelurDTO `json:"penjualan_telur"`
PenjualanTrading BalanceMonitoringTradingDTO `json:"penjualan_trading"`
Pembayaran float64 `json:"pembayaran"`
Aging int `json:"aging"`
AgingRataRata float64 `json:"aging_rata_rata"`
SaldoAkhir float64 `json:"saldo_akhir"`
}
func ToBalanceMonitoringRowDTO(
customer entity.Customer,
saldoAwal float64,
ayam BalanceMonitoringAyamDTO,
telur BalanceMonitoringTelurDTO,
trading BalanceMonitoringTradingDTO,
pembayaran float64,
aging int,
agingRataRata float64,
) BalanceMonitoringRowDTO {
saldoAkhir := saldoAwal + pembayaran - (ayam.Nominal + telur.Nominal + trading.Nominal)
return BalanceMonitoringRowDTO{
Customer: customerDTO.ToCustomerRelationDTO(customer),
SaldoAwal: saldoAwal,
PenjualanAyam: ayam,
PenjualanTelur: telur,
PenjualanTrading: trading,
Pembayaran: pembayaran,
Aging: aging,
AgingRataRata: agingRataRata,
SaldoAkhir: saldoAkhir,
}
}
@@ -6,7 +6,6 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
)
@@ -49,7 +48,6 @@ type RepportExpenseRealisasiDTO struct {
type RepportExpenseListDTO struct {
RepportExpenseBaseDTO
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
@@ -135,15 +133,6 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
totalRealisasi = ns.Realization.Qty * ns.Realization.Price
}
var location *locationDTO.LocationRelationDTO
if ns.Expense != nil && ns.Expense.Location != nil && ns.Expense.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(*ns.Expense.Location)
location = &mapped
} else if ns.Kandang != nil && ns.Kandang.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(ns.Kandang.Location)
location = &mapped
}
// Get kandang data at the main level
var kandang *kandangDTO.KandangRelationDTO
if ns.Kandang != nil && ns.Kandang.Id != 0 {
@@ -153,7 +142,6 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
return RepportExpenseListDTO{
RepportExpenseBaseDTO: baseDTO,
Location: location,
Kandang: kandang,
Pengajuan: ToRepportExpensePengajuanDTO(ns),
Realisasi: realisasi,
-2
View File
@@ -40,7 +40,6 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseDepreciationRepository := repportRepo.NewExpenseDepreciationRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db)
balanceMonitoringRepository := repportRepo.NewBalanceMonitoringRepository(db)
customerRepository := customerRepo.NewCustomerRepository(db)
standardGrowthDetailRepository := productionStandardRepo.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepository := productionStandardRepo.NewProductionStandardDetailRepository(db)
@@ -67,7 +66,6 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
hppPerKandangRepository,
productionResultRepository,
customerPaymentRepository,
balanceMonitoringRepository,
customerRepository,
standardGrowthDetailRepository,
productionStandardDetailRepository,
@@ -1,518 +0,0 @@
package repositories
import (
"context"
"fmt"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type BalanceMonitoringCategoryRow struct {
CustomerID uint `gorm:"column:customer_id"`
AyamQty float64 `gorm:"column:ayam_qty"`
AyamKg float64 `gorm:"column:ayam_kg"`
AyamNominal float64 `gorm:"column:ayam_nominal"`
TelurQty float64 `gorm:"column:telur_qty"`
TelurKg float64 `gorm:"column:telur_kg"`
TelurNominal float64 `gorm:"column:telur_nominal"`
TradingQty float64 `gorm:"column:trading_qty"`
TradingKg float64 `gorm:"column:trading_kg"`
TradingNominal float64 `gorm:"column:trading_nominal"`
}
type BalanceMonitoringAgingRow struct {
CustomerID uint `gorm:"column:customer_id"`
AgingMax int `gorm:"column:aging_max"`
AgingRataRata float64 `gorm:"column:aging_rata_rata"`
}
type BalanceMonitoringGrandTotalsRow struct {
SaldoAwalLifetime float64 `gorm:"column:saldo_awal_lifetime"`
SalesBeforeStart float64 `gorm:"column:sales_before_start"`
PaymentBeforeStart float64 `gorm:"column:payment_before_start"`
AyamQty float64 `gorm:"column:ayam_qty"`
AyamKg float64 `gorm:"column:ayam_kg"`
AyamNominal float64 `gorm:"column:ayam_nominal"`
TelurQty float64 `gorm:"column:telur_qty"`
TelurKg float64 `gorm:"column:telur_kg"`
TelurNominal float64 `gorm:"column:telur_nominal"`
TradingQty float64 `gorm:"column:trading_qty"`
TradingKg float64 `gorm:"column:trading_kg"`
TradingNominal float64 `gorm:"column:trading_nominal"`
PaymentInPeriod float64 `gorm:"column:payment_in_period"`
AgingMax int `gorm:"column:aging_max"`
AgingRataRata float64 `gorm:"column:aging_rata_rata"`
}
type BalanceMonitoringRepository interface {
GetCustomerIDsForBalanceMonitoring(ctx context.Context, offset, limit int, filters *validation.BalanceMonitoringQuery) ([]uint, int64, error)
GetAllFilteredCustomerIDs(ctx context.Context, filters *validation.BalanceMonitoringQuery) ([]uint, error)
GetSaldoAwalLifetime(ctx context.Context, customerIDs []uint) (map[uint]float64, error)
GetSalesTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetSalesByCategoryInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringCategoryRow, error)
GetPaymentTotalsInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error)
GetAgingPerCustomer(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringAgingRow, error)
GetGrandTotals(ctx context.Context, filters *validation.BalanceMonitoringQuery) (BalanceMonitoringGrandTotalsRow, error)
}
type balanceMonitoringRepositoryImpl struct {
db *gorm.DB
}
func NewBalanceMonitoringRepository(db *gorm.DB) BalanceMonitoringRepository {
return &balanceMonitoringRepositoryImpl{db: db}
}
func resolveBalanceMonitoringDateColumn(filterBy string) string {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "realized_at":
return "mdp.delivery_date"
case "sold_at", "":
return "m.so_date"
default:
return "m.so_date"
}
}
func resolveBalanceMonitoringDateRange(filters *validation.BalanceMonitoringQuery) (time.Time, time.Time, error) {
var startDate time.Time
var endDate time.Time
var err error
if strings.TrimSpace(filters.StartDate) != "" {
startDate, err = utils.ParseDateString(filters.StartDate)
if err != nil {
return time.Time{}, time.Time{}, err
}
} else {
startDate = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
}
if strings.TrimSpace(filters.EndDate) != "" {
endDate, err = utils.ParseDateString(filters.EndDate)
if err != nil {
return time.Time{}, time.Time{}, err
}
} else {
endDate = time.Now()
}
return startDate, endDate, nil
}
func resolveBalanceMonitoringSortClause(filters *validation.BalanceMonitoringQuery) string {
direction := "ASC"
if strings.EqualFold(strings.TrimSpace(filters.SortOrder), "desc") {
direction = "DESC"
}
switch strings.ToLower(strings.TrimSpace(filters.SortBy)) {
case "customer":
return "customers.name " + direction
default:
return "customers.name ASC"
}
}
func (r *balanceMonitoringRepositoryImpl) baseCustomerQuery(ctx context.Context, filters *validation.BalanceMonitoringQuery) *gorm.DB {
db := r.db.WithContext(ctx).
Model(&entity.Customer{}).
Where("customers.deleted_at IS NULL")
if len(filters.CustomerIDs) > 0 {
db = db.Where("customers.id IN ?", filters.CustomerIDs)
}
if len(filters.SalesIDs) > 0 {
db = db.Where("EXISTS (SELECT 1 FROM marketings m WHERE m.customer_id = customers.id AND m.deleted_at IS NULL AND m.sales_person_id IN ?)", filters.SalesIDs)
}
if filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil {
scopeSub := r.db.WithContext(ctx).
Table("marketings m").
Select("1").
Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id").
Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id").
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
Where("m.customer_id = customers.id").
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL")
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
scopeSub = scopeSub.Where("w.area_id IN ?", filters.AllowedAreaIDs)
}
}
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
scopeSub = scopeSub.Where("w.location_id IN ?", filters.AllowedLocationIDs)
}
}
db = db.Where("EXISTS (?)", scopeSub)
}
return db
}
func (r *balanceMonitoringRepositoryImpl) GetCustomerIDsForBalanceMonitoring(ctx context.Context, offset, limit int, filters *validation.BalanceMonitoringQuery) ([]uint, int64, error) {
var total int64
if err := r.baseCustomerQuery(ctx, filters).Count(&total).Error; err != nil {
return nil, 0, err
}
if total == 0 {
return []uint{}, 0, nil
}
if offset < 0 {
offset = 0
}
var customerIDs []uint
err := r.baseCustomerQuery(ctx, filters).
Order(resolveBalanceMonitoringSortClause(filters)).
Limit(limit).
Offset(offset).
Pluck("customers.id", &customerIDs).
Error
if err != nil {
return nil, 0, err
}
return customerIDs, total, nil
}
func (r *balanceMonitoringRepositoryImpl) GetAllFilteredCustomerIDs(ctx context.Context, filters *validation.BalanceMonitoringQuery) ([]uint, error) {
var customerIDs []uint
if err := r.baseCustomerQuery(ctx, filters).Pluck("customers.id", &customerIDs).Error; err != nil {
return nil, err
}
return customerIDs, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSaldoAwalLifetime(ctx context.Context, customerIDs []uint) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
Where("party_id IN ?", customerIDs).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, r := range rows {
result[r.CustomerID] = r.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSalesTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, _, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select("m.customer_id AS customer_id, COALESCE(SUM(mdp.total_price), 0) AS total").
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), startDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, _, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err = r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypePenjualan)).
Where("direction = ?", "IN").
Where("party_id IN ?", customerIDs).
Where("DATE(payment_date) < ?", startDate).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetSalesByCategoryInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringCategoryRow, error) {
if len(customerIDs) == 0 {
return map[uint]BalanceMonitoringCategoryRow{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]BalanceMonitoringCategoryRow{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
rows := make([]BalanceMonitoringCategoryRow, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`m.customer_id AS customer_id,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.usage_qty ELSE 0 END), 0) AS ayam_qty,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.total_weight ELSE 0 END), 0) AS ayam_kg,
COALESCE(SUM(CASE WHEN m.marketing_type IN ('AYAM','AYAM_PULLET') THEN mdp.total_price ELSE 0 END), 0) AS ayam_nominal,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.usage_qty ELSE 0 END), 0) AS telur_qty,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.total_weight ELSE 0 END), 0) AS telur_kg,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TELUR' THEN mdp.total_price ELSE 0 END), 0) AS telur_nominal,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.usage_qty ELSE 0 END), 0) AS trading_qty,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.total_weight ELSE 0 END), 0) AS trading_kg,
COALESCE(SUM(CASE WHEN m.marketing_type = 'TRADING' THEN mdp.total_price ELSE 0 END), 0) AS trading_nominal`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), startDate).
Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), endDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]BalanceMonitoringCategoryRow, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetPaymentTotalsInPeriod(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]float64, error) {
if len(customerIDs) == 0 {
return map[uint]float64{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]float64{}, nil
}
type row struct {
CustomerID uint `gorm:"column:customer_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]row, 0)
err = r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS customer_id, COALESCE(SUM(nominal), 0) AS total").
Where("party_type = ?", string(utils.PaymentPartyCustomer)).
Where("transaction_type = ?", string(utils.TransactionTypePenjualan)).
Where("direction = ?", "IN").
Where("party_id IN ?", customerIDs).
Where("DATE(payment_date) >= ?", startDate).
Where("DATE(payment_date) <= ?", endDate).
Group("party_id").
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr.Total
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetAgingPerCustomer(ctx context.Context, customerIDs []uint, filters *validation.BalanceMonitoringQuery) (map[uint]BalanceMonitoringAgingRow, error) {
if len(customerIDs) == 0 {
return map[uint]BalanceMonitoringAgingRow{}, nil
}
startDate, endDate, err := resolveBalanceMonitoringDateRange(filters)
if err != nil {
return map[uint]BalanceMonitoringAgingRow{}, nil
}
dateColumn := resolveBalanceMonitoringDateColumn(filters.FilterBy)
rows := make([]BalanceMonitoringAgingRow, 0)
db := r.db.WithContext(ctx).
Table("marketing_delivery_products mdp").
Select(`m.customer_id AS customer_id,
COALESCE(MAX(GREATEST(CURRENT_DATE - DATE(mdp.delivery_date), 0)), 0) AS aging_max,
COALESCE(
SUM(mdp.total_price * GREATEST(CURRENT_DATE - DATE(mdp.delivery_date), 0))::numeric
/ NULLIF(SUM(mdp.total_price), 0),
0
)::numeric(15,2) AS aging_rata_rata`).
Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
Joins("INNER JOIN marketings m ON m.id = mp.marketing_id").
Where("m.customer_id IN ?", customerIDs).
Where("m.deleted_at IS NULL").
Where("mdp.delivery_date IS NOT NULL").
Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), startDate).
Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), endDate)
if len(filters.SalesIDs) > 0 {
db = db.Where("m.sales_person_id IN ?", filters.SalesIDs)
}
if err := db.Group("m.customer_id").Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]BalanceMonitoringAgingRow, len(rows))
for _, rr := range rows {
result[rr.CustomerID] = rr
}
return result, nil
}
func (r *balanceMonitoringRepositoryImpl) GetGrandTotals(ctx context.Context, filters *validation.BalanceMonitoringQuery) (BalanceMonitoringGrandTotalsRow, error) {
customerIDs, err := r.GetAllFilteredCustomerIDs(ctx, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
if len(customerIDs) == 0 {
return BalanceMonitoringGrandTotalsRow{}, nil
}
saldoAwalLifetimeMap, err := r.GetSaldoAwalLifetime(ctx, customerIDs)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
salesBeforeMap, err := r.GetSalesTotalsBeforeDate(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
paymentBeforeMap, err := r.GetPaymentTotalsBeforeDate(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
categoryMap, err := r.GetSalesByCategoryInPeriod(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
paymentInPeriodMap, err := r.GetPaymentTotalsInPeriod(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
agingMap, err := r.GetAgingPerCustomer(ctx, customerIDs, filters)
if err != nil {
return BalanceMonitoringGrandTotalsRow{}, err
}
totals := BalanceMonitoringGrandTotalsRow{}
for _, total := range saldoAwalLifetimeMap {
totals.SaldoAwalLifetime += total
}
for _, total := range salesBeforeMap {
totals.SalesBeforeStart += total
}
for _, total := range paymentBeforeMap {
totals.PaymentBeforeStart += total
}
for _, cat := range categoryMap {
totals.AyamQty += cat.AyamQty
totals.AyamKg += cat.AyamKg
totals.AyamNominal += cat.AyamNominal
totals.TelurQty += cat.TelurQty
totals.TelurKg += cat.TelurKg
totals.TelurNominal += cat.TelurNominal
totals.TradingQty += cat.TradingQty
totals.TradingKg += cat.TradingKg
totals.TradingNominal += cat.TradingNominal
}
for _, total := range paymentInPeriodMap {
totals.PaymentInPeriod += total
}
for _, aging := range agingMap {
totals.AgingMax += aging.AgingMax
}
weightedSum := 0.0
weightTotal := 0.0
for cid, cat := range categoryMap {
nominal := cat.AyamNominal + cat.TelurNominal + cat.TradingNominal
if aging, ok := agingMap[cid]; ok && nominal > 0 {
weightedSum += nominal * aging.AgingRataRata
weightTotal += nominal
}
}
if weightTotal > 0 {
totals.AgingRataRata = weightedSum / weightTotal
}
return totals, nil
}
@@ -15,16 +15,13 @@ import (
type DebtSupplierRepository interface {
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
GetSuppliersWithDebts(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
GetExpensesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Expense, error)
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
GetExpenseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
}
type debtSupplierRepositoryImpl struct {
@@ -493,218 +490,3 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
return result, nil
}
func (r *debtSupplierRepositoryImpl) latestExpenseApproval(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).
Table("approvals AS a").
Select("a.approvable_id, a.step_number, a.action").
Joins(`
JOIN (
SELECT approvable_id, MAX(action_at) AS latest_action_at
FROM approvals
WHERE approvable_type = ?
GROUP BY approvable_id
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
string(utils.ApprovalWorkflowExpense),
)
}
func (r *debtSupplierRepositoryImpl) baseExpenseSupplierIDs(ctx context.Context, filters *validation.DebtSupplierQuery) *gorm.DB {
db := r.db.WithContext(ctx).
Table("expenses").
Select("DISTINCT expenses.supplier_id").
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("expenses.deleted_at IS NULL")
if len(filters.SupplierIDs) > 0 {
db = db.Where("expenses.supplier_id IN ?", filters.SupplierIDs)
}
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Where("expenses.location_id IN ?", filters.AllowedLocationIDs)
}
}
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Joins("JOIN locations exp_loc ON exp_loc.id = expenses.location_id").
Where("exp_loc.area_id IN ?", filters.AllowedAreaIDs)
}
}
if filters.StartDate != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("DATE(expenses.transaction_date) >= ?", dateFrom)
}
}
if filters.EndDate != "" {
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
db = db.Where("DATE(expenses.transaction_date) <= ?", dateTo)
}
}
return db
}
func (r *debtSupplierRepositoryImpl) GetSuppliersWithDebts(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error) {
purchaseSubquery := r.baseSupplierQuery(ctx, filters).
Select("suppliers.id")
expenseSubquery := r.baseExpenseSupplierIDs(ctx, filters)
db := r.db.WithContext(ctx).
Model(&entity.Supplier{}).
Where("suppliers.id IN (? UNION ?) AND suppliers.deleted_at IS NULL",
purchaseSubquery, expenseSubquery)
var totalSuppliers int64
if err := db.Distinct("suppliers.id").Count(&totalSuppliers).Error; err != nil {
return nil, 0, err
}
if totalSuppliers == 0 {
return []entity.Supplier{}, 0, nil
}
if offset < 0 {
offset = 0
}
type supplierIDResult struct {
ID uint `gorm:"column:id"`
Name string `gorm:"column:name"`
}
var idResults []supplierIDResult
if err := r.db.WithContext(ctx).
Model(&entity.Supplier{}).
Where("suppliers.id IN (? UNION ?) AND suppliers.deleted_at IS NULL",
purchaseSubquery, expenseSubquery).
Select("suppliers.id, suppliers.name").
Group("suppliers.id, suppliers.name").
Order(resolveDebtSupplierSortClause(filters)).
Offset(offset).
Limit(limit).
Scan(&idResults).Error; err != nil {
return nil, 0, err
}
supplierIDs := make([]uint, 0, len(idResults))
for _, r := range idResults {
supplierIDs = append(supplierIDs, r.ID)
}
if len(supplierIDs) == 0 {
return []entity.Supplier{}, totalSuppliers, nil
}
var suppliers []entity.Supplier
if err := r.db.WithContext(ctx).
Where("id IN ?", supplierIDs).
Order(resolveDebtSupplierSortClause(filters)).
Find(&suppliers).Error; err != nil {
return nil, 0, err
}
return suppliers, totalSuppliers, nil
}
func (r *debtSupplierRepositoryImpl) GetExpensesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Expense, error) {
if len(supplierIDs) == 0 {
return []entity.Expense{}, nil
}
db := r.db.WithContext(ctx).
Model(&entity.Expense{}).
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
Where("expenses.supplier_id IN ?", supplierIDs).
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("expenses.deleted_at IS NULL")
if filters.AllowedLocationIDs != nil {
if len(filters.AllowedLocationIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Where("expenses.location_id IN ?", filters.AllowedLocationIDs)
}
}
if filters.AllowedAreaIDs != nil {
if len(filters.AllowedAreaIDs) == 0 {
db = db.Where("1 = 0")
} else {
db = db.Joins("JOIN locations exp_loc ON exp_loc.id = expenses.location_id").
Where("exp_loc.area_id IN ?", filters.AllowedAreaIDs)
}
}
if filters.StartDate != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("DATE(expenses.transaction_date) >= ?", dateFrom)
}
}
if filters.EndDate != "" {
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
db = db.Where("DATE(expenses.transaction_date) <= ?", dateTo)
}
}
var expenses []entity.Expense
if err := db.
Preload("Supplier").
Preload("Nonstocks").
Preload("Location").
Preload("Location.Area").
Order("expenses.transaction_date ASC, expenses.id ASC").
Find(&expenses).Error; err != nil {
return nil, err
}
return expenses, nil
}
func (r *debtSupplierRepositoryImpl) GetExpenseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil
}
dateFrom, err := utils.ParseDateString(filters.StartDate)
if err != nil {
return map[uint]float64{}, nil
}
type expenseTotalRow struct {
SupplierID uint `gorm:"column:supplier_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]expenseTotalRow, 0)
if err := r.db.WithContext(ctx).
Table("expenses").
Select("expenses.supplier_id AS supplier_id, SUM(en.qty * en.price) AS total").
Joins("JOIN expense_nonstocks en ON en.expense_id = expenses.id").
Joins("JOIN (?) AS la ON la.approvable_id = expenses.id", r.latestExpenseApproval(ctx)).
Where("expenses.supplier_id IN ?", supplierIDs).
Where("la.step_number >= ?", uint16(utils.ExpenseStepFinance)).
Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
Where("expenses.deleted_at IS NULL").
Where("DATE(expenses.transaction_date) < ?", dateFrom).
Group("expenses.supplier_id").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, row := range rows {
result[row.SupplierID] = row.Total
}
return result, nil
}
-1
View File
@@ -26,5 +26,4 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/hpp-v2-breakdown", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppV2Breakdown)
route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult)
route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment)
route.Get("/balance-monitoring", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetBalanceMonitoring)
}
@@ -52,7 +52,6 @@ type RepportService interface {
GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
GetBalanceMonitoring(ctx *fiber.Ctx, params *validation.BalanceMonitoringQuery) ([]dto.BalanceMonitoringRowDTO, dto.BalanceMonitoringTotalsDTO, int64, error)
DB() *gorm.DB
}
@@ -75,7 +74,6 @@ type repportService struct {
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
BalanceMonitoringRepo repportRepo.BalanceMonitoringRepository
CustomerRepo customerRepo.CustomerRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
@@ -108,7 +106,6 @@ func NewRepportService(
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository,
balanceMonitoringRepo repportRepo.BalanceMonitoringRepository,
customerRepo customerRepo.CustomerRepository,
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
@@ -132,7 +129,6 @@ func NewRepportService(
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
BalanceMonitoringRepo: balanceMonitoringRepo,
CustomerRepo: customerRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
@@ -1782,7 +1778,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
offset = 0
}
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithDebts(c.Context(), offset, params.Limit, params)
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
if err != nil {
return nil, 0, err
}
@@ -1807,21 +1803,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err
}
expenses, err := s.DebtSupplierRepo.GetExpensesBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
for _, purchase := range purchases {
purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
}
expensesBySupplier := make(map[uint][]entity.Expense, len(supplierIDs))
for _, exp := range expenses {
expensesBySupplier[uint(exp.SupplierId)] = append(expensesBySupplier[uint(exp.SupplierId)], exp)
}
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
for _, payment := range payments {
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
@@ -1837,11 +1823,6 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err
}
initialExpenseTotals, err := s.DebtSupplierRepo.GetExpenseTotalsBeforeDate(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
if err != nil {
return nil, 0, err
@@ -1862,10 +1843,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
CountTotals bool
}
type debtSupplierAllocation struct {
RowIndex int
SortTime time.Time
Amount float64
CalcAging func(endDate time.Time) int
RowIndex int
SortTime time.Time
Amount float64
Purchase entity.Purchase
}
type paymentAllocation struct {
Date time.Time
@@ -1878,7 +1859,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
continue
}
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] - initialExpenseTotals[supplierID])
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID]
total := dto.DebtSupplierTotalDTO{}
@@ -1896,32 +1877,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
DeltaBalance: -row.TotalPrice,
CountTotals: true,
})
capturedPurchase := purchase
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
RowIndex: rowIndex,
SortTime: sortTime,
Amount: row.TotalPrice,
CalcAging: func(endDate time.Time) int { return calculateDebtSupplierAging(capturedPurchase, endDate, location) },
})
}
for _, exp := range expensesBySupplier[supplierID] {
row := buildDebtSupplierExpenseRow(exp, now, location)
sortTime := exp.TransactionDate.In(location)
rowIndex := len(combinedRows)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
SortTime: sortTime,
Order: 0,
DeltaBalance: -row.TotalPrice,
CountTotals: true,
})
capturedExp := exp
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
RowIndex: rowIndex,
SortTime: sortTime,
Amount: row.TotalPrice,
CalcAging: func(endDate time.Time) int { return calculateExpenseAging(capturedExp, endDate, location) },
RowIndex: rowIndex,
SortTime: sortTime,
Amount: row.TotalPrice,
Purchase: purchase,
})
}
@@ -1986,7 +1946,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
if remaining[purchaseIndex] <= 0.000001 {
allocation := purchaseAllocations[purchaseIndex]
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
combinedRows[allocation.RowIndex].Row.Aging = allocation.CalcAging(pay.Date)
combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
purchaseIndex++
}
}
@@ -2260,62 +2220,6 @@ func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Locatio
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
}
func buildDebtSupplierExpenseRow(exp entity.Expense, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
txDate := exp.TransactionDate.In(loc)
dateStr := txDate.Format("2006-01-02")
startDay := time.Date(txDate.Year(), txDate.Month(), txDate.Day(), 0, 0, 0, 0, loc)
endDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
aging := 0
if !startDay.IsZero() && !endDay.Before(startDay) {
aging = int(endDay.Sub(startDay).Hours() / 24)
}
totalPrice := 0.0
for _, ns := range exp.Nonstocks {
totalPrice += ns.Qty * ns.Price
}
var area *areaDTO.AreaRelationDTO
if exp.Location != nil && exp.Location.Area.Id != 0 {
mapped := areaDTO.ToAreaRelationDTO(exp.Location.Area)
area = &mapped
}
poNumber := ""
if strings.TrimSpace(exp.PoNumber) != "" {
poNumber = exp.PoNumber
}
return dto.DebtSupplierRowDTO{
PrNumber: exp.ReferenceNumber,
PoNumber: poNumber,
PoDate: dateStr,
ReceivedDate: dateStr,
Aging: aging,
Area: area,
Warehouse: nil,
DueDate: "-",
DueStatus: "-",
TotalPrice: totalPrice,
PaymentPrice: 0,
DebtPrice: 0,
Status: "Belum Lunas",
TravelNumber: "-",
Balance: 0,
}
}
func calculateExpenseAging(exp entity.Expense, endDate time.Time, loc *time.Location) int {
start := exp.TransactionDate.In(loc)
startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, loc)
stopDay := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
if stopDay.Before(startDay) {
return 0
}
return int(stopDay.Sub(startDay).Hours() / 24)
}
func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
@@ -2989,163 +2893,3 @@ func parseOptionalFloat64(raw string) (*float64, error) {
return &value, nil
}
func (s *repportService) GetBalanceMonitoring(ctx *fiber.Ctx, params *validation.BalanceMonitoringQuery) ([]dto.BalanceMonitoringRowDTO, dto.BalanceMonitoringTotalsDTO, int64, error) {
if params.SortBy == "" {
params.SortBy = "customer"
}
if params.SortOrder == "" {
params.SortOrder = "asc"
}
if params.FilterBy == "" {
params.FilterBy = "sold_at"
}
if params.Page < 1 {
params.Page = 1
}
if params.Limit < 1 {
params.Limit = 10
}
if err := s.Validate.Struct(params); err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
locationScope, err := m.ResolveLocationScope(ctx, s.DB())
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
areaScope, err := m.ResolveAreaScope(ctx, s.DB())
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
if locationScope.Restrict {
params.AllowedLocationIDs = toInt64Slice(locationScope.IDs)
}
if areaScope.Restrict {
params.AllowedAreaIDs = toInt64Slice(areaScope.IDs)
}
offset := (params.Page - 1) * params.Limit
customerIDs, total, err := s.BalanceMonitoringRepo.GetCustomerIDsForBalanceMonitoring(ctx.Context(), offset, params.Limit, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
if len(customerIDs) == 0 {
emptyTotals, gtErr := s.computeBalanceMonitoringTotals(ctx.Context(), params)
if gtErr != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, gtErr
}
return []dto.BalanceMonitoringRowDTO{}, emptyTotals, total, nil
}
saldoAwalLifetimeMap, err := s.BalanceMonitoringRepo.GetSaldoAwalLifetime(ctx.Context(), customerIDs)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
salesBeforeMap, err := s.BalanceMonitoringRepo.GetSalesTotalsBeforeDate(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
paymentBeforeMap, err := s.BalanceMonitoringRepo.GetPaymentTotalsBeforeDate(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
categoryMap, err := s.BalanceMonitoringRepo.GetSalesByCategoryInPeriod(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
paymentInPeriodMap, err := s.BalanceMonitoringRepo.GetPaymentTotalsInPeriod(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
agingMap, err := s.BalanceMonitoringRepo.GetAgingPerCustomer(ctx.Context(), customerIDs, params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
customers, err := s.CustomerRepo.GetByIDs(ctx.Context(), customerIDs, func(db *gorm.DB) *gorm.DB {
return db.Preload("Pic")
})
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
customerMap := make(map[uint]entity.Customer, len(customers))
for _, c := range customers {
customerMap[c.Id] = c
}
result := make([]dto.BalanceMonitoringRowDTO, 0, len(customerIDs))
for _, customerID := range customerIDs {
customer, ok := customerMap[customerID]
if !ok {
continue
}
saldoAwal := saldoAwalLifetimeMap[customerID] + paymentBeforeMap[customerID] - salesBeforeMap[customerID]
category := categoryMap[customerID]
ayam := dto.BalanceMonitoringAyamDTO{
Ekor: category.AyamQty,
Kg: category.AyamKg,
Nominal: category.AyamNominal,
}
telur := dto.BalanceMonitoringTelurDTO{
Butir: category.TelurQty,
Kg: category.TelurKg,
Nominal: category.TelurNominal,
}
trading := dto.BalanceMonitoringTradingDTO{
Qty: category.TradingQty,
Kg: category.TradingKg,
Nominal: category.TradingNominal,
}
pembayaran := paymentInPeriodMap[customerID]
aging := agingMap[customerID]
row := dto.ToBalanceMonitoringRowDTO(customer, saldoAwal, ayam, telur, trading, pembayaran, aging.AgingMax, aging.AgingRataRata)
result = append(result, row)
}
totals, err := s.computeBalanceMonitoringTotals(ctx.Context(), params)
if err != nil {
return nil, dto.BalanceMonitoringTotalsDTO{}, 0, err
}
return result, totals, total, nil
}
func (s *repportService) computeBalanceMonitoringTotals(ctx context.Context, params *validation.BalanceMonitoringQuery) (dto.BalanceMonitoringTotalsDTO, error) {
grand, err := s.BalanceMonitoringRepo.GetGrandTotals(ctx, params)
if err != nil {
return dto.BalanceMonitoringTotalsDTO{}, err
}
saldoAwal := grand.SaldoAwalLifetime + grand.PaymentBeforeStart - grand.SalesBeforeStart
saldoAkhir := saldoAwal + grand.PaymentInPeriod - (grand.AyamNominal + grand.TelurNominal + grand.TradingNominal)
return dto.BalanceMonitoringTotalsDTO{
SaldoAwal: saldoAwal,
PenjualanAyam: dto.BalanceMonitoringAyamDTO{
Ekor: grand.AyamQty,
Kg: grand.AyamKg,
Nominal: grand.AyamNominal,
},
PenjualanTelur: dto.BalanceMonitoringTelurDTO{
Butir: grand.TelurQty,
Kg: grand.TelurKg,
Nominal: grand.TelurNominal,
},
PenjualanTrading: dto.BalanceMonitoringTradingDTO{
Qty: grand.TradingQty,
Kg: grand.TradingKg,
Nominal: grand.TradingNominal,
},
Pembayaran: grand.PaymentInPeriod,
Aging: grand.AgingMax,
AgingRataRata: grand.AgingRataRata,
SaldoAkhir: saldoAkhir,
}, nil
}
@@ -116,17 +116,3 @@ type CustomerPaymentQuery struct {
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
}
type BalanceMonitoringQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,gt=0"`
CustomerIDs []uint `query:"-" validate:"omitempty,dive,gt=0"`
SalesIDs []uint `query:"-" validate:"omitempty,dive,gt=0"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=sold_at realized_at"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=customer"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
AllowedAreaIDs []int64 `query:"-"`
AllowedLocationIDs []int64 `query:"-"`
}