mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 23:35:43 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2b3366ba | |||
| e015e20b5c | |||
| d92d28c892 | |||
| 60bdd4a31a | |||
| cce0d44f83 | |||
| c8623e2f7c |
@@ -98,6 +98,7 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context
|
|||||||
return db.
|
return db.
|
||||||
Preload("Expense").
|
Preload("Expense").
|
||||||
Preload("Expense.Supplier").
|
Preload("Expense.Supplier").
|
||||||
|
Preload("Expense.Location").
|
||||||
Preload("Kandang").
|
Preload("Kandang").
|
||||||
Preload("Kandang.Location").
|
Preload("Kandang.Location").
|
||||||
Preload("Nonstock").
|
Preload("Nonstock").
|
||||||
|
|||||||
@@ -485,6 +485,13 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
|
|||||||
return err
|
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 single customer mode (only 1 customer ID), return without pagination
|
||||||
if len(customerIDs) == 1 {
|
if len(customerIDs) == 1 {
|
||||||
return ctx.Status(fiber.StatusOK).
|
return ctx.Status(fiber.StatusOK).
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
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
|
||||||
|
initialFormatted := formatCPRupiah(item.InitialBalance)
|
||||||
|
if err := file.SetCellValue(sheet, "N2", initialFormatted); 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]string{
|
||||||
|
"A": "Total",
|
||||||
|
"G": formatCPIDInteger(item.Summary.TotalQty),
|
||||||
|
"H": formatCPIDInteger(item.Summary.TotalWeight),
|
||||||
|
"K": formatCPRupiah(item.Summary.TotalFinalAmount),
|
||||||
|
"L": formatCPRupiah(item.Summary.TotalGrandAmount),
|
||||||
|
"M": formatCPRupiah(item.Summary.TotalPayment),
|
||||||
|
"N": formatCPRupiah(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
|
||||||
|
}
|
||||||
|
initialFormatted := formatCPRupiah(item.InitialBalance)
|
||||||
|
if err := file.SetCellValue(sheet, "O"+saldoStr, initialFormatted); 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]string{
|
||||||
|
"A": name,
|
||||||
|
"B": "Total",
|
||||||
|
"H": formatCPIDInteger(item.Summary.TotalQty),
|
||||||
|
"I": formatCPIDInteger(item.Summary.TotalWeight),
|
||||||
|
"L": formatCPRupiah(item.Summary.TotalFinalAmount),
|
||||||
|
"M": formatCPRupiah(item.Summary.TotalGrandAmount),
|
||||||
|
"N": formatCPRupiah(item.Summary.TotalPayment),
|
||||||
|
"O": formatCPRupiah(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),
|
||||||
|
formatCPRupiah(row.UnitPrice),
|
||||||
|
formatCPRupiah(row.FinalPrice),
|
||||||
|
formatCPRupiah(row.TotalPrice),
|
||||||
|
formatCPRupiah(row.PaymentAmount),
|
||||||
|
formatCPRupiah(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 formatCPRupiah(v float64) string {
|
||||||
|
const nbsp = " "
|
||||||
|
if v < 0 {
|
||||||
|
return "-Rp" + nbsp + formatCPIDInteger(-v)
|
||||||
|
}
|
||||||
|
return "Rp" + nbsp + formatCPIDInteger(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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"
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
)
|
)
|
||||||
@@ -48,6 +49,7 @@ type RepportExpenseRealisasiDTO struct {
|
|||||||
|
|
||||||
type RepportExpenseListDTO struct {
|
type RepportExpenseListDTO struct {
|
||||||
RepportExpenseBaseDTO
|
RepportExpenseBaseDTO
|
||||||
|
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
|
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
|
||||||
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
|
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
|
||||||
@@ -133,6 +135,15 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
|
|||||||
totalRealisasi = ns.Realization.Qty * ns.Realization.Price
|
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
|
// Get kandang data at the main level
|
||||||
var kandang *kandangDTO.KandangRelationDTO
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
if ns.Kandang != nil && ns.Kandang.Id != 0 {
|
if ns.Kandang != nil && ns.Kandang.Id != 0 {
|
||||||
@@ -142,6 +153,7 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
|
|||||||
|
|
||||||
return RepportExpenseListDTO{
|
return RepportExpenseListDTO{
|
||||||
RepportExpenseBaseDTO: baseDTO,
|
RepportExpenseBaseDTO: baseDTO,
|
||||||
|
Location: location,
|
||||||
Kandang: kandang,
|
Kandang: kandang,
|
||||||
Pengajuan: ToRepportExpensePengajuanDTO(ns),
|
Pengajuan: ToRepportExpensePengajuanDTO(ns),
|
||||||
Realisasi: realisasi,
|
Realisasi: realisasi,
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ import (
|
|||||||
|
|
||||||
type DebtSupplierRepository interface {
|
type DebtSupplierRepository interface {
|
||||||
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
|
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)
|
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)
|
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, 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)
|
GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
|
||||||
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
|
GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
|
||||||
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (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)
|
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 {
|
type debtSupplierRepositoryImpl struct {
|
||||||
@@ -490,3 +493,218 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Cont
|
|||||||
|
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1782,7 +1782,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
|
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithDebts(c.Context(), offset, params.Limit, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -1807,11 +1807,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
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))
|
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
||||||
for _, purchase := range purchases {
|
for _, purchase := range purchases {
|
||||||
purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase)
|
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))
|
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||||
for _, payment := range payments {
|
for _, payment := range payments {
|
||||||
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
||||||
@@ -1827,6 +1837,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
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)
|
initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -1847,10 +1862,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
CountTotals bool
|
CountTotals bool
|
||||||
}
|
}
|
||||||
type debtSupplierAllocation struct {
|
type debtSupplierAllocation struct {
|
||||||
RowIndex int
|
RowIndex int
|
||||||
SortTime time.Time
|
SortTime time.Time
|
||||||
Amount float64
|
Amount float64
|
||||||
Purchase entity.Purchase
|
CalcAging func(endDate time.Time) int
|
||||||
}
|
}
|
||||||
type paymentAllocation struct {
|
type paymentAllocation struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
@@ -1863,7 +1878,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] - initialExpenseTotals[supplierID])
|
||||||
items := purchasesBySupplier[supplierID]
|
items := purchasesBySupplier[supplierID]
|
||||||
paymentItems := paymentsBySupplier[supplierID]
|
paymentItems := paymentsBySupplier[supplierID]
|
||||||
total := dto.DebtSupplierTotalDTO{}
|
total := dto.DebtSupplierTotalDTO{}
|
||||||
@@ -1881,11 +1896,32 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
DeltaBalance: -row.TotalPrice,
|
DeltaBalance: -row.TotalPrice,
|
||||||
CountTotals: true,
|
CountTotals: true,
|
||||||
})
|
})
|
||||||
|
capturedPurchase := purchase
|
||||||
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{
|
||||||
RowIndex: rowIndex,
|
RowIndex: rowIndex,
|
||||||
SortTime: sortTime,
|
SortTime: sortTime,
|
||||||
Amount: row.TotalPrice,
|
Amount: row.TotalPrice,
|
||||||
Purchase: purchase,
|
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) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1950,7 +1986,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
if remaining[purchaseIndex] <= 0.000001 {
|
if remaining[purchaseIndex] <= 0.000001 {
|
||||||
allocation := purchaseAllocations[purchaseIndex]
|
allocation := purchaseAllocations[purchaseIndex]
|
||||||
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
|
combinedRows[allocation.RowIndex].Row.Status = "Lunas"
|
||||||
combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
|
combinedRows[allocation.RowIndex].Row.Aging = allocation.CalcAging(pay.Date)
|
||||||
purchaseIndex++
|
purchaseIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2224,6 +2260,62 @@ func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Locatio
|
|||||||
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
|
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) {
|
func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
|||||||
Reference in New Issue
Block a user