mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 13:55:43 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 495f5f5cc1 | |||
| 71e80634b1 | |||
| af2b3366ba | |||
| e015e20b5c | |||
| d92d28c892 | |||
| 60bdd4a31a | |||
| cce0d44f83 | |||
| 6fc4ad5773 | |||
| e61625d2f7 | |||
| 32c34be2c6 | |||
| d2aa3ebac7 |
@@ -98,6 +98,7 @@ 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,6 +5,7 @@ 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"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const transactionExcelExportFetchLimit = 99999999
|
||||
|
||||
type TransactionController struct {
|
||||
TransactionService service.TransactionService
|
||||
}
|
||||
@@ -107,6 +110,14 @@ 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
|
||||
@@ -149,6 +160,32 @@ 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")
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
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,max=100,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,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,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -153,7 +152,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), formatMarketingRupiah(sumMarketingGrandTotal(item.SalesOrder))); err != nil {
|
||||
if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), sumMarketingGrandTotal(item.SalesOrder)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "F"+strconv.Itoa(rowNumber), formatMarketingProducts(item.SalesOrder)); err != nil {
|
||||
@@ -266,40 +265,6 @@ 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)
|
||||
|
||||
@@ -542,9 +542,15 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||
}
|
||||
|
||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
|
||||
|
||||
marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
|
||||
@@ -630,6 +636,23 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
}
|
||||
}
|
||||
|
||||
if latestApproval != nil && latestApproval.StepNumber == uint16(utils.MarketingDeliveryOrder) {
|
||||
action := entity.ApprovalActionUpdated
|
||||
_, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowMarketing,
|
||||
id,
|
||||
utils.MarketingStepSalesOrder,
|
||||
&action,
|
||||
actorID,
|
||||
nil)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval to Sales Order")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -516,7 +516,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowMarketing,
|
||||
id,
|
||||
approvalutils.ApprovalStep(latestApproval.StepNumber),
|
||||
utils.MarketingStepPengajuan,
|
||||
&action,
|
||||
actorID,
|
||||
nil)
|
||||
|
||||
@@ -91,11 +91,9 @@ 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")),
|
||||
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")),
|
||||
StartDate: strings.TrimSpace(c.Query("start_date")),
|
||||
EndDate: strings.TrimSpace(c.Query("end_date")),
|
||||
FilterBy: strings.TrimSpace(c.Query("filter_by")),
|
||||
SupplierID: uint(c.QueryInt("supplier_id", 0)),
|
||||
AreaID: uint(c.QueryInt("area_id", 0)),
|
||||
LocationID: uint(c.QueryInt("location_id", 0)),
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -43,15 +42,13 @@ 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, grandTotals); err != nil {
|
||||
if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := file.SetPanes(purchaseExportSheetName, &excelize.Panes{
|
||||
@@ -80,9 +77,17 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
|
||||
"F": 22,
|
||||
"G": 22,
|
||||
"H": 32,
|
||||
"I": 18,
|
||||
"J": 18,
|
||||
"K": 24,
|
||||
"I": 10,
|
||||
"J": 12,
|
||||
"K": 16,
|
||||
"L": 16,
|
||||
"M": 22,
|
||||
"N": 12,
|
||||
"O": 16,
|
||||
"P": 16,
|
||||
"Q": 18,
|
||||
"R": 18,
|
||||
"S": 24,
|
||||
}
|
||||
|
||||
for col, width := range columnWidths {
|
||||
@@ -99,17 +104,25 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
|
||||
|
||||
func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
|
||||
headers := []string{
|
||||
"PR Number",
|
||||
"PO Number",
|
||||
"Tanggal PO",
|
||||
"Tanggal Terima",
|
||||
"Supplier",
|
||||
"Lokasi",
|
||||
"Gudang",
|
||||
"Product",
|
||||
"Status",
|
||||
"Grand Total",
|
||||
"Notes",
|
||||
"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
|
||||
}
|
||||
|
||||
for i, header := range headers {
|
||||
@@ -137,34 +150,36 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return file.SetCellStyle(sheet, "A1", "K1", headerStyle)
|
||||
return file.SetCellStyle(sheet, "A1", "S1", headerStyle)
|
||||
}
|
||||
|
||||
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase, grandTotals map[uint]float64) error {
|
||||
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase) 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, total); err != nil {
|
||||
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, &sumL, &sumP, &sumQ); err != nil {
|
||||
return err
|
||||
}
|
||||
rowIdx++
|
||||
continue
|
||||
}
|
||||
for it := range purchase.Items {
|
||||
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], total); err != nil {
|
||||
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], &sumL, &sumP, &sumQ); err != nil {
|
||||
return err
|
||||
}
|
||||
rowIdx++
|
||||
}
|
||||
}
|
||||
|
||||
lastRow := rowIdx - 1
|
||||
lastDataRow := rowIdx - 1
|
||||
|
||||
dataStyle, err := file.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
@@ -181,7 +196,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellStyle(sheet, "A2", "K"+strconv.Itoa(lastRow), dataStyle); err != nil {
|
||||
if err := file.SetCellStyle(sheet, "A2", "S"+strconv.Itoa(lastDataRow), dataStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -200,14 +215,17 @@ 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 file.SetCellStyle(sheet, "J2", "J"+strconv.Itoa(lastRow), moneyStyle)
|
||||
return addPurchaseExportSumRow(file, sheet, rowIdx, sumL, sumP, sumQ)
|
||||
}
|
||||
|
||||
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, grandTotal float64) error {
|
||||
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, sumL, sumP, sumQ *float64) error {
|
||||
row := strconv.Itoa(rowIdx)
|
||||
|
||||
// Purchase-level columns (repeat across rows of the same purchase)
|
||||
// Purchase-level columns (repeat for every item row of the same purchase)
|
||||
if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(purchase.PrNumber)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -220,26 +238,40 @@ 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, "I"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
|
||||
if err := file.SetCellValue(sheet, "R"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
if err := file.SetCellValue(sheet, "S"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Item-level columns
|
||||
if item == nil {
|
||||
for _, col := range []string{"D", "F", "G", "H"} {
|
||||
for _, col := range []string{"D", "F", "G", "H", "J", "M"} {
|
||||
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
|
||||
}
|
||||
@@ -252,20 +284,96 @@ 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 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
|
||||
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
|
||||
}
|
||||
return result
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func safePurchaseExportEntitySupplierName(purchase *entity.Purchase) string {
|
||||
@@ -296,6 +404,24 @@ 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 "-"
|
||||
@@ -338,37 +464,3 @@ 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,9 +22,8 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
|
||||
nil,
|
||||
"catatan",
|
||||
[]entity.PurchaseItem{
|
||||
buildPurchaseItemForExportTest(11, "Pakan Starter", 1000000, "Location A"),
|
||||
buildPurchaseItemForExportTest(12, "Vitamin A", 350000, "Location B"),
|
||||
buildPurchaseItemForExportTest(11, "Pakan Starter", 0, ""),
|
||||
buildPurchaseItemForExportTest(11, "Pakan Starter", 500, 2, 1000000, "Location A", "kg"),
|
||||
buildPurchaseItemForExportTest(12, "Vitamin A", 350, 1, 350000, "Location B", "botol"),
|
||||
},
|
||||
),
|
||||
buildPurchaseForExportTest(
|
||||
@@ -37,7 +36,7 @@ func TestBuildPurchaseExportWorkbookHeadersAndRows(t *testing.T) {
|
||||
ptrApprovalAction(entity.ApprovalActionRejected),
|
||||
"",
|
||||
[]entity.PurchaseItem{
|
||||
buildPurchaseItemForExportTest(21, "Obat X", 75000, ""),
|
||||
buildPurchaseItemForExportTest(21, "Obat X", 75000, 1, 75000, "", ""),
|
||||
},
|
||||
),
|
||||
})
|
||||
@@ -51,16 +50,27 @@ 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": "Supplier",
|
||||
"E1": "Lokasi",
|
||||
"F1": "Status",
|
||||
"G1": "Grand Total",
|
||||
"H1": "Products",
|
||||
"I1": "Notes",
|
||||
"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",
|
||||
}
|
||||
for cell, expected := range expectedHeaders {
|
||||
got, err := file.GetCellValue(purchaseExportSheetName, cell)
|
||||
@@ -72,24 +82,46 @@ 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, "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")
|
||||
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, "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", "-")
|
||||
// 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")
|
||||
}
|
||||
|
||||
func assertPurchaseCellEquals(t *testing.T, file *excelize.File, cell, expected string) {
|
||||
@@ -144,13 +176,20 @@ func buildPurchaseForExportTest(
|
||||
}
|
||||
}
|
||||
|
||||
func buildPurchaseItemForExportTest(productID uint, productName string, totalPrice float64, locationName string) entity.PurchaseItem {
|
||||
func buildPurchaseItemForExportTest(productID uint, productName string, price, totalQty, totalPrice float64, locationName, uomName string) entity.PurchaseItem {
|
||||
uomID := uint(0)
|
||||
if uomName != "" {
|
||||
uomID = productID + 2000
|
||||
}
|
||||
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},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,15 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type PurchaseDetailDTO struct {
|
||||
@@ -69,6 +72,8 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -127,6 +132,8 @@ 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
|
||||
@@ -173,15 +180,21 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
}
|
||||
|
||||
var (
|
||||
poExpedition = make([]PoExpeditionDTO, 0)
|
||||
location *locationDTO.LocationRelationDTO
|
||||
area *areaDTO.AreaRelationDTO
|
||||
receivedDate *time.Time
|
||||
poExpedition = make([]PoExpeditionDTO, 0)
|
||||
location *locationDTO.LocationRelationDTO
|
||||
area *areaDTO.AreaRelationDTO
|
||||
receivedDate *time.Time
|
||||
productsTotal float64
|
||||
expeditionTotal float64
|
||||
)
|
||||
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)
|
||||
@@ -235,6 +248,9 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
LatestApproval: latestApproval,
|
||||
ProductsTotal: productsTotal,
|
||||
ExpeditionTotal: expeditionTotal,
|
||||
GrandTotalAll: productsTotal + expeditionTotal,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,33 +145,16 @@ 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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
dateStart, dateEnd, err := parsePurchaseDateRangeForQuery(params.StartDate, params.EndDate, "date")
|
||||
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)
|
||||
@@ -187,23 +170,41 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
db = db.Where("supplier_id = ?", params.SupplierID)
|
||||
}
|
||||
|
||||
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)
|
||||
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 scope.Restrict {
|
||||
@@ -2238,30 +2239,29 @@ func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []ent
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePoDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, error) {
|
||||
func parsePurchaseDateRangeForQuery(fromStr, toStr, fieldName 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("po_date_from must use format YYYY-MM-DD")
|
||||
return nil, nil, errors.New(fieldName + "_from must use format YYYY-MM-DD")
|
||||
}
|
||||
fromValue := parsed
|
||||
fromPtr = &fromValue
|
||||
fromPtr = &parsed
|
||||
}
|
||||
|
||||
if strings.TrimSpace(toStr) != "" {
|
||||
parsed, err := utils.ParseDateString(strings.TrimSpace(toStr))
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("po_date_to must use format YYYY-MM-DD")
|
||||
return nil, nil, errors.New(fieldName + "_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("po_date_from must be earlier than po_date_to")
|
||||
return nil, nil, errors.New(fieldName + "_from must be earlier than " + fieldName + "_to")
|
||||
}
|
||||
|
||||
return fromPtr, toPtr, nil
|
||||
|
||||
@@ -75,12 +75,10 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -214,8 +214,7 @@ func writeCustomerPaymentSheet(file *excelize.File, sheet string, item dto.Custo
|
||||
}
|
||||
|
||||
// Row 2: saldo awal
|
||||
initialFormatted := formatCPRupiah(item.InitialBalance)
|
||||
if err := file.SetCellValue(sheet, "N2", initialFormatted); err != nil {
|
||||
if err := file.SetCellValue(sheet, "N2", item.InitialBalance); err != nil {
|
||||
return err
|
||||
}
|
||||
if item.InitialBalance < 0 {
|
||||
@@ -248,14 +247,14 @@ func writeCustomerPaymentSheet(file *excelize.File, sheet string, item dto.Custo
|
||||
totalRowNum := len(item.Rows) + 3
|
||||
totalRowStr := fmt.Sprintf("%d", totalRowNum)
|
||||
|
||||
totalCells := map[string]string{
|
||||
totalCells := map[string]interface{}{
|
||||
"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),
|
||||
"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 {
|
||||
@@ -369,8 +368,7 @@ func writeCustomerPaymentAllRows(file *excelize.File, sheet string, items []dto.
|
||||
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 {
|
||||
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 {
|
||||
@@ -409,15 +407,15 @@ func writeCustomerPaymentAllRows(file *excelize.File, sheet string, items []dto.
|
||||
|
||||
// Total row
|
||||
totalStr := fmt.Sprintf("%d", currentRow)
|
||||
totalCells := map[string]string{
|
||||
totalCells := map[string]interface{}{
|
||||
"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),
|
||||
"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 {
|
||||
@@ -453,11 +451,11 @@ func customerPaymentRowCells(row dto.CustomerPaymentReportRow, seq int) []interf
|
||||
formatCPIDInteger(row.Qty),
|
||||
formatCPIDInteger(row.Weight),
|
||||
formatCPAvg(row.AverageWeight),
|
||||
formatCPRupiah(row.UnitPrice),
|
||||
formatCPRupiah(row.FinalPrice),
|
||||
formatCPRupiah(row.TotalPrice),
|
||||
formatCPRupiah(row.PaymentAmount),
|
||||
formatCPRupiah(row.AccountsReceivable),
|
||||
row.UnitPrice,
|
||||
row.FinalPrice,
|
||||
row.TotalPrice,
|
||||
row.PaymentAmount,
|
||||
row.AccountsReceivable,
|
||||
safeCPText(row.Status),
|
||||
joinCPStrings(row.PickupInfo),
|
||||
safeCPText(row.SalesPerson),
|
||||
@@ -546,13 +544,6 @@ func formatCPIDInteger(v float64) 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 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -197,9 +196,9 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte
|
||||
item.Qty,
|
||||
item.AverageWeightKg,
|
||||
item.TotalWeightKg,
|
||||
formatMarketingRupiah(item.SalesPricePerKg),
|
||||
formatMarketingRupiah(item.HppPricePerKg),
|
||||
formatMarketingRupiah(item.SalesAmount),
|
||||
item.SalesPricePerKg,
|
||||
item.HppPricePerKg,
|
||||
item.SalesAmount,
|
||||
}
|
||||
|
||||
for colIdx, val := range values {
|
||||
@@ -229,13 +228,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, formatMarketingRupiah(summary.AverageSalesPrice)); err != nil {
|
||||
if err := file.SetCellValue(sheet, "O"+totalRow, summary.AverageSalesPrice); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "P"+totalRow, formatMarketingRupiah(summary.TotalHppPricePerKg)); err != nil {
|
||||
if err := file.SetCellValue(sheet, "P"+totalRow, summary.TotalHppPricePerKg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "Q"+totalRow, formatMarketingRupiah(float64(summary.TotalSalesAmount))); err != nil {
|
||||
if err := file.SetCellValue(sheet, "Q"+totalRow, float64(summary.TotalSalesAmount)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -333,30 +332,3 @@ 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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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,6 +6,7 @@ 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"
|
||||
)
|
||||
@@ -48,6 +49,7 @@ 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"`
|
||||
@@ -133,6 +135,15 @@ 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 {
|
||||
@@ -142,6 +153,7 @@ func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNo
|
||||
|
||||
return RepportExpenseListDTO{
|
||||
RepportExpenseBaseDTO: baseDTO,
|
||||
Location: location,
|
||||
Kandang: kandang,
|
||||
Pengajuan: ToRepportExpensePengajuanDTO(ns),
|
||||
Realisasi: realisasi,
|
||||
|
||||
@@ -40,6 +40,7 @@ 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)
|
||||
@@ -66,6 +67,7 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
hppPerKandangRepository,
|
||||
productionResultRepository,
|
||||
customerPaymentRepository,
|
||||
balanceMonitoringRepository,
|
||||
customerRepository,
|
||||
standardGrowthDetailRepository,
|
||||
productionStandardDetailRepository,
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
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,13 +15,16 @@ 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 {
|
||||
@@ -490,3 +493,218 @@ 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
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ 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,6 +52,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -74,6 +75,7 @@ type repportService struct {
|
||||
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
||||
ProductionResultRepo repportRepo.ProductionResultRepository
|
||||
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
|
||||
BalanceMonitoringRepo repportRepo.BalanceMonitoringRepository
|
||||
CustomerRepo customerRepo.CustomerRepository
|
||||
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
|
||||
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
|
||||
@@ -106,6 +108,7 @@ func NewRepportService(
|
||||
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
||||
productionResultRepo repportRepo.ProductionResultRepository,
|
||||
customerPaymentRepo repportRepo.CustomerPaymentRepository,
|
||||
balanceMonitoringRepo repportRepo.BalanceMonitoringRepository,
|
||||
customerRepo customerRepo.CustomerRepository,
|
||||
standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository,
|
||||
productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository,
|
||||
@@ -129,6 +132,7 @@ func NewRepportService(
|
||||
HppPerKandangRepo: hppPerKandangRepo,
|
||||
ProductionResultRepo: productionResultRepo,
|
||||
CustomerPaymentRepo: customerPaymentRepo,
|
||||
BalanceMonitoringRepo: balanceMonitoringRepo,
|
||||
CustomerRepo: customerRepo,
|
||||
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||
@@ -1778,7 +1782,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -1803,11 +1807,21 @@ 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)
|
||||
@@ -1823,6 +1837,11 @@ 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
|
||||
@@ -1843,10 +1862,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
CountTotals bool
|
||||
}
|
||||
type debtSupplierAllocation struct {
|
||||
RowIndex int
|
||||
SortTime time.Time
|
||||
Amount float64
|
||||
Purchase entity.Purchase
|
||||
RowIndex int
|
||||
SortTime time.Time
|
||||
Amount float64
|
||||
CalcAging func(endDate time.Time) int
|
||||
}
|
||||
type paymentAllocation struct {
|
||||
Date time.Time
|
||||
@@ -1859,7 +1878,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
||||
continue
|
||||
}
|
||||
|
||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID])
|
||||
initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] - initialExpenseTotals[supplierID])
|
||||
items := purchasesBySupplier[supplierID]
|
||||
paymentItems := paymentsBySupplier[supplierID]
|
||||
total := dto.DebtSupplierTotalDTO{}
|
||||
@@ -1877,11 +1896,32 @@ 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,
|
||||
Purchase: purchase,
|
||||
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) },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1946,7 +1986,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 = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location)
|
||||
combinedRows[allocation.RowIndex].Row.Aging = allocation.CalcAging(pay.Date)
|
||||
purchaseIndex++
|
||||
}
|
||||
}
|
||||
@@ -2220,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)
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -2893,3 +2989,163 @@ 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,3 +116,17 @@ 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:"-"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user