mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-334-Report-closing-hpp-expedisi
This commit is contained in:
@@ -63,13 +63,14 @@ func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
|
|||||||
updates["note"] = *note
|
updates["note"] = *note
|
||||||
}
|
}
|
||||||
|
|
||||||
q := r.DB().WithContext(ctx).
|
baseDB := r.DB()
|
||||||
|
if modifier != nil {
|
||||||
|
baseDB = modifier(baseDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := baseDB.WithContext(ctx).
|
||||||
Model(&entity.StockAllocation{}).
|
Model(&entity.StockAllocation{}).
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
|
||||||
|
|
||||||
if modifier != nil {
|
|
||||||
q = modifier(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
return q.Updates(updates).Error
|
return q.Updates(updates).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingController struct {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
ClosingService service.ClosingService
|
||||||
|
SapronakService service.SapronakService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||||
return &ClosingController{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
ClosingService: closingService,
|
||||||
|
SapronakService: sapronakService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := &validation.SapronakQuery{
|
query := &validation.ClosingSapronakQuery{
|
||||||
Type: strings.ToLower(c.Query("type")),
|
Type: strings.ToLower(c.Query("type")),
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
@@ -189,6 +191,61 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
flag := c.Query("flag", "")
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(param)
|
||||||
|
if err != nil || projectID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get perhitungan sapronak per project successfully",
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||||
|
projectParam := c.Params("project_flock_id")
|
||||||
|
kandangParam := c.Params("project_flock_kandang_id")
|
||||||
|
flag := c.Query("flag", "")
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(projectParam)
|
||||||
|
if err != nil || projectID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||||
|
}
|
||||||
|
pfkID, err := strconv.Atoi(kandangParam)
|
||||||
|
if err != nil || pfkID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get perhitungan sapronak per kandang successfully",
|
||||||
|
Data: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ type SalesDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PenjualanRealisasiResponseDTO struct {
|
type PenjualanRealisasiResponseDTO struct {
|
||||||
ProjectType string `json:"project_type"`
|
Sales []SalesDTO `json:"sales"`
|
||||||
FlockId uint `json:"flock_id"`
|
|
||||||
Period int `json:"period"`
|
|
||||||
Sales []SalesDTO `json:"sales"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
@@ -87,12 +84,10 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
period := extractPeriodFromRealisasi(e)
|
|
||||||
return PenjualanRealisasiResponseDTO{
|
return PenjualanRealisasiResponseDTO{
|
||||||
ProjectType: projectType,
|
|
||||||
FlockId: projectFlockID,
|
Sales: ToSalesDTOs(e),
|
||||||
Period: period,
|
|
||||||
Sales: ToSalesDTOs(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,95 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakDetailDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Tanggal *time.Time `json:"tanggal,omitempty"`
|
||||||
|
NoReferensi string `json:"no_referensi,omitempty"`
|
||||||
|
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
||||||
|
QtyMasuk float64 `json:"qty_masuk"`
|
||||||
|
QtyKeluar float64 `json:"qty_keluar"`
|
||||||
|
Harga float64 `json:"harga"`
|
||||||
|
Nilai float64 `json:"nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakGroupDTO struct {
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
Items []SapronakDetailDTO `json:"items"`
|
||||||
|
TotalMasuk float64 `json:"total_masuk"`
|
||||||
|
TotalKeluar float64 `json:"total_keluar"`
|
||||||
|
SaldoAkhir float64 `json:"saldo_akhir"`
|
||||||
|
TotalNilai float64 `json:"total_nilai"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakItemDTO struct {
|
||||||
|
ProductID uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
IncomingQty float64 `json:"incoming_qty"`
|
||||||
|
IncomingValue float64 `json:"incoming_value"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
UsageValue float64 `json:"usage_value"`
|
||||||
|
RemainingQty float64 `json:"remaining_qty"`
|
||||||
|
AveragePrice float64 `json:"average_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakReportDTO struct {
|
||||||
|
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||||
|
ProjectFlockID uint `json:"project_flock_id"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
KandangID uint `json:"kandang_id"`
|
||||||
|
KandangName string `json:"kandang_name"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StartDate *time.Time `json:"start_date,omitempty"`
|
||||||
|
EndDate *time.Time `json:"end_date,omitempty"`
|
||||||
|
TotalIncomingValue float64 `json:"total_incoming_value"`
|
||||||
|
TotalUsageValue float64 `json:"total_usage_value"`
|
||||||
|
Items []SapronakItemDTO `json:"items"`
|
||||||
|
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified view for project-level sapronak response
|
||||||
|
type SapronakCategoryRowDTO struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryTotalDTO struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
QtyIn float64 `json:"qty_in"`
|
||||||
|
QtyOut float64 `json:"qty_out"`
|
||||||
|
QtyUsed float64 `json:"qty_used"`
|
||||||
|
AvgUnitPrice float64 `json:"avg_unit_price"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakCategoryDTO struct {
|
||||||
|
Rows []SapronakCategoryRowDTO `json:"rows"`
|
||||||
|
Total SapronakCategoryTotalDTO `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakProjectAggregatedDTO struct {
|
||||||
|
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
||||||
|
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||||
|
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||||
|
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type ClosingSapronakItemDTO struct {
|
type ClosingSapronakItemDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
@@ -24,3 +113,140 @@ type ClosingSapronakDTO struct {
|
|||||||
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := reports[0]
|
||||||
|
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||||
|
result := SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
|
if report == nil {
|
||||||
|
report = &SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||||
|
|
||||||
|
byFlag := map[string]**SapronakCategoryDTO{}
|
||||||
|
if filter == "" || filter == "DOC" {
|
||||||
|
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["DOC"] = &result.Doc
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "OVK" {
|
||||||
|
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["OVK"] = &result.Ovk
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PAKAN" {
|
||||||
|
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PAKAN"] = &result.Pakan
|
||||||
|
}
|
||||||
|
if filter == "" || filter == "PULLET" {
|
||||||
|
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||||
|
byFlag["PULLET"] = &result.Pullet
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate := func(t *time.Time) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range report.Groups {
|
||||||
|
flagKey := strings.ToUpper(group.Flag)
|
||||||
|
ptr := byFlag[flagKey]
|
||||||
|
if ptr == nil || *ptr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target := *ptr
|
||||||
|
|
||||||
|
rowIndexByProduct := make(map[string]int)
|
||||||
|
|
||||||
|
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
||||||
|
if idx, ok := rowIndexByProduct[productKey]; ok {
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
target.Rows = append(target.Rows, base)
|
||||||
|
idx := len(target.Rows) - 1
|
||||||
|
rowIndexByProduct[productKey] = idx
|
||||||
|
return &target.Rows[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, item := range group.Items {
|
||||||
|
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
||||||
|
baseRow := SapronakCategoryRowDTO{
|
||||||
|
ID: idx + 1,
|
||||||
|
Date: formatDate(item.Tanggal),
|
||||||
|
ReferenceNumber: item.NoReferensi,
|
||||||
|
Description: item.ProductName,
|
||||||
|
ProductCategory: item.ProductName,
|
||||||
|
UnitPrice: item.Harga,
|
||||||
|
Notes: "-",
|
||||||
|
}
|
||||||
|
|
||||||
|
row := getOrCreateRow(productKey, baseRow)
|
||||||
|
|
||||||
|
switch strings.ToLower(item.JenisTransaksi) {
|
||||||
|
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
case "pemakaian", "adjustment keluar":
|
||||||
|
row.QtyUsed += item.QtyKeluar
|
||||||
|
case "mutasi keluar":
|
||||||
|
row.QtyOut += item.QtyKeluar
|
||||||
|
default:
|
||||||
|
row.QtyIn += item.QtyMasuk
|
||||||
|
row.TotalAmount += item.Nilai
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.QtyIn > 0 {
|
||||||
|
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range target.Rows {
|
||||||
|
target.Rows[i].ID = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
||||||
|
if cat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var qtyIn, qtyOut, qtyUsed, total float64
|
||||||
|
for _, r := range cat.Rows {
|
||||||
|
qtyIn += r.QtyIn
|
||||||
|
qtyOut += r.QtyOut
|
||||||
|
qtyUsed += r.QtyUsed
|
||||||
|
total += r.TotalAmount
|
||||||
|
}
|
||||||
|
avg := 0.0
|
||||||
|
if qtyIn > 0 {
|
||||||
|
avg = total / qtyIn
|
||||||
|
}
|
||||||
|
cat.Total = SapronakCategoryTotalDTO{
|
||||||
|
Label: label,
|
||||||
|
QtyIn: qtyIn,
|
||||||
|
QtyOut: qtyOut,
|
||||||
|
QtyUsed: qtyUsed,
|
||||||
|
AvgUnitPrice: avg,
|
||||||
|
TotalAmount: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTotals(result.Doc, "TOTAL DOC")
|
||||||
|
buildTotals(result.Ovk, "TOTAL OVK")
|
||||||
|
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||||
|
buildTotals(result.Pullet, "TOTAL PULLET")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||||
@@ -33,7 +34,8 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||||
|
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService)
|
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ type ClosingRepository interface {
|
|||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
||||||
|
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||||
|
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||||
|
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
|
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClosingRepositoryImpl struct {
|
type ClosingRepositoryImpl struct {
|
||||||
@@ -271,3 +279,370 @@ JOIN warehouses w ON w.id = pw.warehouse_id
|
|||||||
WHERE pw.project_flock_kandang_id IN ?
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SapronakIncomingRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
Value float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakUsageRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Qty float64
|
||||||
|
DefaultPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakDetailRow struct {
|
||||||
|
ProductID uint
|
||||||
|
ProductName string
|
||||||
|
Flag string
|
||||||
|
Date *time.Time
|
||||||
|
Reference string
|
||||||
|
QtyIn float64
|
||||||
|
QtyOut float64
|
||||||
|
Price float64
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||||
|
|
||||||
|
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||||
|
for _, j := range joins {
|
||||||
|
if strings.TrimSpace(j) != "" {
|
||||||
|
db = db.Joins(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func sapronakFlags(flags ...utils.FlagType) []string {
|
||||||
|
out := make([]string, len(flags))
|
||||||
|
for i, f := range flags {
|
||||||
|
out[i] = string(f)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
|
||||||
|
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
|
||||||
|
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
|
||||||
|
)
|
||||||
|
|
||||||
|
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
|
||||||
|
m := make(map[uint][]SapronakDetailRow)
|
||||||
|
for _, row := range rows {
|
||||||
|
m[row.ProductID] = append(m[row.ProductID], row)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows := make([]SapronakDetailRow, 0)
|
||||||
|
if err := db.Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groupSapronakDetails(rows), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Usage (summary + details)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) usageQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).Table(table).Select(`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(usage_qty), 0) AS qty,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) ([]SapronakUsageRow, error) {
|
||||||
|
rows := make([]SapronakUsageRow, 0)
|
||||||
|
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
|
||||||
|
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) detailQuery(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) *gorm.DB {
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table(table).
|
||||||
|
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
return db.Select(selectSQL).Where(where, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
table string,
|
||||||
|
pwJoinCond string,
|
||||||
|
joins []string,
|
||||||
|
selectSQL string,
|
||||||
|
where string,
|
||||||
|
args ...any,
|
||||||
|
) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||||
|
if pfkID == 0 {
|
||||||
|
return []SapronakUsageRow{}, nil
|
||||||
|
}
|
||||||
|
return r.fetchSapronakUsage(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"recording_stocks rs",
|
||||||
|
"pw.id = rs.product_warehouse_id",
|
||||||
|
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
r.record_datetime AS date,
|
||||||
|
CAST(r.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsUsage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return r.fetchSapronakDetails(
|
||||||
|
ctx,
|
||||||
|
"project_chickins pc",
|
||||||
|
"pw.id = pc.product_warehouse_id",
|
||||||
|
nil,
|
||||||
|
`
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pc.chick_in_date AS date,
|
||||||
|
CAST(pc.id AS TEXT) AS reference,
|
||||||
|
0 AS qty_in,
|
||||||
|
COALESCE(pc.usage_qty,0) AS qty_out,
|
||||||
|
COALESCE(p.product_price,0) AS price
|
||||||
|
`,
|
||||||
|
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||||
|
pfkID,
|
||||||
|
sapronakFlagsChickin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||||
|
return r.withCtx(ctx).
|
||||||
|
Table("purchase_items AS pi").
|
||||||
|
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||||
|
Joins("JOIN products p ON p.id = pi.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Where("pi.received_date IS NOT NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
|
||||||
|
rows := make([]SapronakIncomingRow, 0)
|
||||||
|
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||||
|
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||||
|
COALESCE(p.product_price, 0) AS default_price
|
||||||
|
`)
|
||||||
|
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||||
|
return scanAndGroupDetails(
|
||||||
|
r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||||
|
pi.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
pi.received_date AS date,
|
||||||
|
COALESCE(po.po_number, '') AS reference,
|
||||||
|
COALESCE(pi.total_qty,0) AS qty_in,
|
||||||
|
0 AS qty_out,
|
||||||
|
COALESCE(pi.price,0) AS price
|
||||||
|
`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockLogSapronakRow struct {
|
||||||
|
ID uint `gorm:"column:id"`
|
||||||
|
ProductID uint `gorm:"column:product_id"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
Flag string `gorm:"column:flag"`
|
||||||
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||||
|
Increase float64 `gorm:"column:increase"`
|
||||||
|
Decrease float64 `gorm:"column:decrease"`
|
||||||
|
Price float64 `gorm:"column:price"`
|
||||||
|
MovementNumber string `gorm:"column:movement_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
|
||||||
|
rows := make([]stockLogSapronakRow, 0)
|
||||||
|
|
||||||
|
movementSelect := "'' AS movement_number"
|
||||||
|
joins := []string{}
|
||||||
|
if withMovement {
|
||||||
|
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
|
||||||
|
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.withCtx(ctx).
|
||||||
|
Table("stock_logs sl").
|
||||||
|
Select(`
|
||||||
|
sl.id AS id,
|
||||||
|
pw.product_id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
f.name AS flag,
|
||||||
|
sl.created_at AS created_at,
|
||||||
|
COALESCE(sl.increase,0) AS increase,
|
||||||
|
COALESCE(sl.decrease,0) AS decrease,
|
||||||
|
COALESCE(p.product_price,0) AS price,
|
||||||
|
` + movementSelect + `
|
||||||
|
`).
|
||||||
|
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||||
|
Joins("JOIN products p ON p.id = pw.product_id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||||
|
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
|
||||||
|
|
||||||
|
db = applyJoins(db, joins...)
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Where("sl.loggable_type = ?", logType).
|
||||||
|
Where("w.kandang_id = ?", kandangID).
|
||||||
|
Where("f.name IN ?", sapronakFlagsAll).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
|
||||||
|
in := make(map[uint][]SapronakDetailRow)
|
||||||
|
out := make(map[uint][]SapronakDetailRow)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
base := SapronakDetailRow{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
Date: row.CreatedAt,
|
||||||
|
Reference: refFn(row),
|
||||||
|
Price: row.Price,
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.Increase > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyIn = row.Increase
|
||||||
|
in[row.ProductID] = append(in[row.ProductID], d)
|
||||||
|
}
|
||||||
|
if row.Decrease > 0 {
|
||||||
|
d := base
|
||||||
|
d.QtyOut = row.Decrease
|
||||||
|
out[row.ProductID] = append(out[row.ProductID], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return in, out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||||
|
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||||
|
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("TRF-%d", row.ID)
|
||||||
|
})
|
||||||
|
return in, out, nil
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||||
ctrl := controller.NewClosingController(s)
|
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closings")
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||||
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||||
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||||
|
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||||
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||||
route.Get("/:project_flock_id/expedition-hpp", ctrl.GetExpeditionHPP)
|
route.Get("/:project_flock_id/expedition-hpp", ctrl.GetExpeditionHPP)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type ClosingService interface {
|
|||||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +169,13 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
|||||||
return &summary, nil
|
return &summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if params == nil {
|
if params == nil {
|
||||||
params = &validation.SapronakQuery{}
|
params = &validation.ClosingSapronakQuery{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.Page == 0 {
|
if params.Page == 0 {
|
||||||
@@ -331,7 +331,7 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
|||||||
minStep = rec.StepNumber
|
minStep = rec.StepNumber
|
||||||
statusProject = rec.StepName
|
statusProject = rec.StepName
|
||||||
}
|
}
|
||||||
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
|
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||||
completed++
|
completed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,681 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SapronakService interface {
|
||||||
|
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||||
|
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSapronakService(
|
||||||
|
repo repository.ClosingRepository,
|
||||||
|
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) SapronakService {
|
||||||
|
return &sapronakService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ProjectFlockKandangRepo: pfkRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||||
|
}
|
||||||
|
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(reports) <= 1 {
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||||
|
return []dto.SapronakReportDTO{combined}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||||
|
if projectFlockID == 0 || pfkID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
|
ProjectFlockID: projectFlockID,
|
||||||
|
ProjectFlockKandangID: pfkID,
|
||||||
|
Status: "all",
|
||||||
|
Flag: flag,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, res := range results {
|
||||||
|
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||||
|
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(pfks) == 0 {
|
||||||
|
return []dto.SapronakReportDTO{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||||
|
if filterStatus == "" {
|
||||||
|
filterStatus = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||||
|
for _, pfk := range pfks {
|
||||||
|
status := "closing"
|
||||||
|
if pfk.ClosedAt == nil {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||||
|
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, dto.SapronakReportDTO{
|
||||||
|
ProjectFlockKandangID: pfk.Id,
|
||||||
|
ProjectFlockID: pfk.ProjectFlockId,
|
||||||
|
ProjectName: pfk.ProjectFlock.FlockName,
|
||||||
|
KandangID: pfk.KandangId,
|
||||||
|
KandangName: pfk.Kandang.Name,
|
||||||
|
Period: pfk.Period,
|
||||||
|
Status: status,
|
||||||
|
StartDate: nil,
|
||||||
|
EndDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||||
|
Preload("ProjectFlock").
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("Chickins")
|
||||||
|
|
||||||
|
if params != nil {
|
||||||
|
if params.ProjectFlockID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||||
|
}
|
||||||
|
if params.KandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||||
|
}
|
||||||
|
if params.ProjectFlockKandangID > 0 {
|
||||||
|
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pfks []entity.ProjectFlockKandang
|
||||||
|
if err := db.Find(&pfks).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||||
|
}
|
||||||
|
return pfks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||||
|
if len(reports) == 0 {
|
||||||
|
return dto.SapronakReportDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalIncoming float64
|
||||||
|
totalUsage float64
|
||||||
|
projectName = reports[0].ProjectName
|
||||||
|
)
|
||||||
|
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO)
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range reports {
|
||||||
|
totalIncoming += r.TotalIncomingValue
|
||||||
|
totalUsage += r.TotalUsageValue
|
||||||
|
|
||||||
|
for _, it := range r.Items {
|
||||||
|
cur := itemMap[it.ProductID]
|
||||||
|
if cur.ProductID == 0 {
|
||||||
|
cur.ProductID = it.ProductID
|
||||||
|
cur.ProductName = it.ProductName
|
||||||
|
cur.Flag = it.Flag
|
||||||
|
}
|
||||||
|
cur.IncomingQty += it.IncomingQty
|
||||||
|
cur.IncomingValue += it.IncomingValue
|
||||||
|
cur.UsageQty += it.UsageQty
|
||||||
|
cur.UsageValue += it.UsageValue
|
||||||
|
if cur.IncomingQty >= cur.UsageQty {
|
||||||
|
cur.RemainingQty = cur.IncomingQty - cur.UsageQty
|
||||||
|
} else {
|
||||||
|
cur.RemainingQty = 0
|
||||||
|
}
|
||||||
|
if cur.IncomingQty > 0 {
|
||||||
|
cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
|
||||||
|
} else {
|
||||||
|
cur.AveragePrice = it.AveragePrice
|
||||||
|
}
|
||||||
|
itemMap[it.ProductID] = cur
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, g := range r.Groups {
|
||||||
|
agg := ensureGroup(g.Flag)
|
||||||
|
agg.TotalMasuk += g.TotalMasuk
|
||||||
|
agg.TotalKeluar += g.TotalKeluar
|
||||||
|
agg.SaldoAkhir += g.SaldoAkhir
|
||||||
|
agg.TotalNilai += g.TotalNilai
|
||||||
|
agg.Items = append(agg.Items, g.Items...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
for _, it := range itemMap {
|
||||||
|
items = append(items, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.SapronakReportDTO{
|
||||||
|
ProjectFlockID: projectID,
|
||||||
|
ProjectName: projectName,
|
||||||
|
Status: "combined",
|
||||||
|
StartDate: nil,
|
||||||
|
TotalIncomingValue: totalIncoming,
|
||||||
|
TotalUsageValue: totalUsage,
|
||||||
|
Items: items,
|
||||||
|
Groups: groups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
|
||||||
|
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
|
||||||
|
for _, row := range incomingRows {
|
||||||
|
incoming[row.ProductID] = row
|
||||||
|
}
|
||||||
|
usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
|
||||||
|
for _, row := range usageRows {
|
||||||
|
usage[row.ProductID] = row
|
||||||
|
}
|
||||||
|
return incoming, usage
|
||||||
|
}
|
||||||
|
|
||||||
|
type sapronakDetailMaps struct {
|
||||||
|
Incoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
Usage map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjIncoming map[uint][]dto.SapronakDetailDTO
|
||||||
|
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||||
|
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSapronakDetails(
|
||||||
|
incomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
usageRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjIncomingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||||
|
) sapronakDetailMaps {
|
||||||
|
result := sapronakDetailMaps{
|
||||||
|
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
Usage: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||||
|
for pid, rows := range src {
|
||||||
|
for _, r := range rows {
|
||||||
|
d := dto.SapronakDetailDTO{
|
||||||
|
ProductID: r.ProductID,
|
||||||
|
ProductName: r.ProductName,
|
||||||
|
Flag: r.Flag,
|
||||||
|
Tanggal: r.Date,
|
||||||
|
NoReferensi: r.Reference,
|
||||||
|
JenisTransaksi: jenis,
|
||||||
|
Harga: r.Price,
|
||||||
|
}
|
||||||
|
if masuk {
|
||||||
|
d.QtyMasuk = r.QtyIn
|
||||||
|
d.Nilai = r.QtyIn * r.Price
|
||||||
|
} else {
|
||||||
|
d.QtyKeluar = r.QtyOut
|
||||||
|
d.Nilai = r.QtyOut * r.Price
|
||||||
|
}
|
||||||
|
target[pid] = append(target[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||||
|
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||||
|
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||||
|
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||||
|
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||||
|
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||||
|
// For sapronak closing report we intentionally ignore date range
|
||||||
|
// and aggregate all historical transactions for the kandang/project.
|
||||||
|
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||||
|
matchesFlag := func(f string) bool {
|
||||||
|
if filterFlag == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.ToUpper(f) == filterFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
// For project flocks with category GROWING, pullet usage from chickin
|
||||||
|
// should not be counted yet. Only when category is LAYING we allow
|
||||||
|
// pullet usage to contribute to qty_used.
|
||||||
|
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
|
||||||
|
|
||||||
|
if !isLaying {
|
||||||
|
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
|
||||||
|
for _, row := range chickinUsageRows {
|
||||||
|
if strings.ToUpper(row.Flag) == "DOC" {
|
||||||
|
filteredUsage = append(filteredUsage, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageRows = filteredUsage
|
||||||
|
|
||||||
|
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
for _, d := range rows {
|
||||||
|
if strings.ToUpper(d.Flag) == "DOC" {
|
||||||
|
filteredDetail[pid] = append(filteredDetail[pid], d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickinUsageDetailsRows = filteredDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsageRows := append(usageRows, chickinUsageRows...)
|
||||||
|
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
|
||||||
|
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||||
|
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||||
|
|
||||||
|
for pid, rows := range chickinUsageDetailsRows {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||||
|
incomingDetails := detailMaps.Incoming
|
||||||
|
usageDetails := detailMaps.Usage
|
||||||
|
adjIncoming := detailMaps.AdjIncoming
|
||||||
|
adjOutgoing := detailMaps.AdjOutgoing
|
||||||
|
transIncoming := detailMaps.TransferIn
|
||||||
|
transOutgoing := detailMaps.TransferOut
|
||||||
|
|
||||||
|
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||||
|
if g, ok := groupMap[flag]; ok {
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||||
|
return groupMap[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range incoming {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
avgPrice := row.DefaultPrice
|
||||||
|
if row.Qty > 0 && row.Value > 0 {
|
||||||
|
avgPrice = row.Value / row.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
Flag: row.Flag,
|
||||||
|
IncomingQty: row.Qty,
|
||||||
|
IncomingValue: row.Value,
|
||||||
|
RemainingQty: row.Qty,
|
||||||
|
AveragePrice: avgPrice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range usage {
|
||||||
|
if !matchesFlag(row.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[row.ProductID]
|
||||||
|
price := existing.AveragePrice
|
||||||
|
if price == 0 {
|
||||||
|
price = row.DefaultPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
usageValue := row.Qty * price
|
||||||
|
|
||||||
|
existing.ProductID = row.ProductID
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = row.ProductName
|
||||||
|
}
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = row.Flag
|
||||||
|
}
|
||||||
|
existing.AveragePrice = price
|
||||||
|
existing.UsageQty += row.Qty
|
||||||
|
existing.UsageValue += usageValue
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap[row.ProductID] = existing
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.UsageQty += d.QtyKeluar
|
||||||
|
existing.UsageValue += d.Nilai
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
for _, d := range details {
|
||||||
|
if !matchesFlag(d.Flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing := itemMap[productID]
|
||||||
|
if existing.Flag == "" {
|
||||||
|
existing.Flag = d.Flag
|
||||||
|
}
|
||||||
|
if existing.ProductName == "" {
|
||||||
|
existing.ProductName = d.ProductName
|
||||||
|
}
|
||||||
|
existing.IncomingQty += d.QtyMasuk
|
||||||
|
existing.IncomingValue += d.Nilai
|
||||||
|
if existing.IncomingQty > 0 {
|
||||||
|
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||||
|
}
|
||||||
|
if existing.IncomingQty >= existing.UsageQty {
|
||||||
|
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||||
|
} else {
|
||||||
|
existing.RemainingQty = 0
|
||||||
|
}
|
||||||
|
itemMap[productID] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||||
|
var totalIncoming, totalUsage float64
|
||||||
|
for _, item := range itemMap {
|
||||||
|
totalIncoming += item.IncomingValue
|
||||||
|
totalUsage += item.UsageValue
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range incomingDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range usageDetails {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range adjOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transIncoming {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalMasuk += d.QtyMasuk
|
||||||
|
group.TotalNilai += d.Nilai
|
||||||
|
group.SaldoAkhir += d.QtyMasuk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for productID, details := range transOutgoing {
|
||||||
|
flag := ""
|
||||||
|
name := ""
|
||||||
|
if item, ok := itemMap[productID]; ok {
|
||||||
|
flag = item.Flag
|
||||||
|
name = item.ProductName
|
||||||
|
}
|
||||||
|
if !matchesFlag(flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
group := ensureGroup(flag)
|
||||||
|
for _, d := range details {
|
||||||
|
d.Flag = flag
|
||||||
|
d.ProductName = name
|
||||||
|
group.Items = append(group.Items, d)
|
||||||
|
group.TotalKeluar += d.QtyKeluar
|
||||||
|
group.SaldoAkhir -= d.QtyKeluar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||||
|
for _, g := range groupMap {
|
||||||
|
groups = append(groups, *g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, groups, totalIncoming, totalUsage, nil
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ const (
|
|||||||
SapronakTypeOutgoing = "outgoing"
|
SapronakTypeOutgoing = "outgoing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SapronakQuery struct {
|
type ClosingSapronakQuery struct {
|
||||||
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
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,max=100,gt=0"`
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type CountSapronakQuery struct {
|
||||||
|
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||||
|
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
Status string `query:"status" validate:"omitempty,oneof=active closing all"`
|
||||||
|
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
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"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ type ExpenseRealizationRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
|
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -50,3 +53,83 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte
|
|||||||
Find(&realizations).Error
|
Find(&realizations).Error
|
||||||
return realizations, err
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
||||||
|
var realizations []entity.ExpenseRealization
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ExpenseRealization{}).
|
||||||
|
Preload("ExpenseNonstock", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("Expense").
|
||||||
|
Preload("Expense.Supplier").
|
||||||
|
Preload("Kandang").
|
||||||
|
Preload("Kandang.Location").
|
||||||
|
Preload("Nonstock")
|
||||||
|
}).
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||||
|
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
||||||
|
|
||||||
|
if filters.Search != "" {
|
||||||
|
db = db.Where("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?",
|
||||||
|
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.Category != "" {
|
||||||
|
db = db.Where("expenses.category = ?", filters.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.SupplierId > 0 {
|
||||||
|
db = db.Where("expenses.supplier_id = ?", filters.SupplierId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.KandangId > 0 {
|
||||||
|
db = db.Where("expense_nonstocks.kandang_id = ?", filters.KandangId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.ProjectFlockKandangId > 0 {
|
||||||
|
db = db.Where("expense_nonstocks.project_flock_kandang_id = ?", filters.ProjectFlockKandangId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.NonstockId > 0 {
|
||||||
|
db = db.Where("expense_nonstocks.nonstock_id = ?", filters.NonstockId)
|
||||||
|
}
|
||||||
|
|
||||||
|
locationID := filters.LocationId
|
||||||
|
areaID := filters.AreaId
|
||||||
|
|
||||||
|
if locationID > 0 || areaID > 0 {
|
||||||
|
db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id")
|
||||||
|
|
||||||
|
if locationID > 0 {
|
||||||
|
db = db.Where("kandangs.location_id = ?", uint(locationID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if areaID > 0 {
|
||||||
|
db = db.Joins("JOIN locations ON locations.id = kandangs.location_id").
|
||||||
|
Where("locations.area_id = ?", uint(areaID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.RealizationDate != "" {
|
||||||
|
if realizationDate, err := utils.ParseDateString(filters.RealizationDate); err == nil {
|
||||||
|
db = db.Where("DATE(expenses.realization_date) = ?", realizationDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Offset(offset).
|
||||||
|
Limit(limit).
|
||||||
|
Order("expense_realizations.created_at DESC").
|
||||||
|
Find(&realizations).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return realizations, total, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,10 +33,8 @@ type ProductWarehouseDTO struct {
|
|||||||
|
|
||||||
type AdjustmentRelationDTO struct {
|
type AdjustmentRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
TransactionType string `json:"transaction_type"`
|
Increase float64 `json:"increase"`
|
||||||
Quantity float64 `json:"quantity"`
|
Decrease float64 `json:"decrease"`
|
||||||
BeforeQuantity float64 `json:"before_quantity"`
|
|
||||||
AfterQuantity float64 `json:"after_quantity"`
|
|
||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||||
@@ -104,12 +102,10 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
|
|
||||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
// TransactionType: e.LoggableType,
|
|
||||||
// Quantity: e.Q,
|
|
||||||
// BeforeQuantity: e.BeforeQuantity,
|
|
||||||
// AfterQuantity: e.AfterQuantity,
|
|
||||||
Note: e.Notes,
|
Note: e.Notes,
|
||||||
|
Increase: e.Increase,
|
||||||
|
Decrease: e.Decrease,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
|
|||||||
|
|
||||||
route := v1.Group("/adjustments")
|
route := v1.Group("/adjustments")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
// Standard CRUD routes following master data pattern
|
|
||||||
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
route.Get("/", ctrl.AdjustmentHistory)
|
||||||
route.Post("/", ctrl.Adjustment) // Create adjustment
|
route.Post("/", ctrl.Adjustment)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
|
|
||||||
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
|
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
}
|
}
|
||||||
if query.WarehouseID > 0 && !isWarehousesExist {
|
if query.WarehouseID > 0 && !isWarehousesExist {
|
||||||
|
|||||||
+17
-5
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
|
|||||||
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error)
|
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -93,7 +93,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con
|
|||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC")
|
Order("product_warehouses.id DESC")
|
||||||
|
|
||||||
// preload relations so nested Product and Warehouse are populated
|
// preload relations so nested Product and Warehouse are populated
|
||||||
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
||||||
@@ -199,10 +199,21 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
productID uint,
|
productID uint,
|
||||||
warehouseID uint,
|
warehouseID uint,
|
||||||
|
projectFlockKandangID *uint,
|
||||||
createdBy uint,
|
createdBy uint,
|
||||||
) (uint, error) {
|
) (uint, error) {
|
||||||
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
// Backfill project_flock_kandang_id when it's missing and caller provides one.
|
||||||
|
if projectFlockKandangID != nil && (record.ProjectFlockKandangId == nil || *record.ProjectFlockKandangId == 0) {
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProductWarehouse{}).
|
||||||
|
Where("id = ?", record.Id).
|
||||||
|
Update("project_flock_kandang_id", *projectFlockKandangID).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
record.ProjectFlockKandangId = projectFlockKandangID
|
||||||
|
}
|
||||||
return record.Id, nil
|
return record.Id, nil
|
||||||
}
|
}
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -210,9 +221,10 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity := &entity.ProductWarehouse{
|
entity := &entity.ProductWarehouse{
|
||||||
ProductId: productID,
|
ProductId: productID,
|
||||||
WarehouseId: warehouseID,
|
WarehouseId: warehouseID,
|
||||||
Quantity: 0,
|
ProjectFlockKandangId: projectFlockKandangID,
|
||||||
|
Quantity: 0,
|
||||||
// CreatedBy: uint(createdBy),
|
// CreatedBy: uint(createdBy),
|
||||||
}
|
}
|
||||||
// if entity.CreatedBy == 0 {
|
// if entity.CreatedBy == 0 {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.
|
return db.
|
||||||
Preload("CreatedUser").
|
Preload("CreatedUser").
|
||||||
@@ -96,13 +97,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
||||||
|
|
||||||
return transfers, total, nil
|
return transfers, total, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||||
var transfer entity.StockTransfer
|
var transfer entity.StockTransfer
|
||||||
|
|
||||||
// gunakan repo secara langsung
|
|
||||||
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
return s.withRelations(db)
|
return s.withRelations(db)
|
||||||
})
|
})
|
||||||
@@ -120,10 +119,9 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
|
||||||
pwIDs := make([]uint, 0, len(req.Products))
|
pwIDs := make([]uint, 0, len(req.Products))
|
||||||
|
|
||||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
||||||
@@ -139,6 +137,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
pwIDs = append(pwIDs, sourcePW.Id)
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
s.StockTransferRepo.DB(),
|
s.StockTransferRepo.DB(),
|
||||||
@@ -152,7 +151,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
|
|
||||||
deliveryQtyMap := make(map[uint]float64)
|
deliveryQtyMap := make(map[uint]float64)
|
||||||
for _, delivery := range req.Deliveries {
|
for _, delivery := range req.Deliveries {
|
||||||
for _, prod := range delivery.Products {
|
for _, prod := range delivery.Products {
|
||||||
@@ -160,7 +158,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cek: qty delivery tidak boleh melebihi qty di root
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
if deliveryQtyMap[product.ProductID] > product.ProductQty {
|
if deliveryQtyMap[product.ProductID] > product.ProductQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest,
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
@@ -168,7 +165,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cek suplier id caegory BOP cek by id
|
|
||||||
for _, delivery := range req.Deliveries {
|
for _, delivery := range req.Deliveries {
|
||||||
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
|
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,8 +178,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate movement number
|
|
||||||
// Format: PND-MBU-00001
|
|
||||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get next movement number: %+v", err)
|
s.Log.Errorf("Failed to get next movement number: %+v", err)
|
||||||
@@ -201,17 +195,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
CreatedBy: uint64(actorID),
|
CreatedBy: uint64(actorID),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the transfer entity to the database
|
|
||||||
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
// Insert header
|
|
||||||
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create stock transfer: %+v", err)
|
s.Log.Errorf("Failed to create stock transfer: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
||||||
|
|
||||||
// insert ke details
|
|
||||||
var details []*entity.StockTransferDetail
|
var details []*entity.StockTransferDetail
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
details = append(details, &entity.StockTransferDetail{
|
details = append(details, &entity.StockTransferDetail{
|
||||||
@@ -226,7 +217,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
||||||
|
|
||||||
// Tambahkan proses insert delivery
|
|
||||||
var deliveries []*entity.StockTransferDelivery
|
var deliveries []*entity.StockTransferDelivery
|
||||||
for _, delivery := range req.Deliveries {
|
for _, delivery := range req.Deliveries {
|
||||||
deliveries = append(deliveries, &entity.StockTransferDelivery{
|
deliveries = append(deliveries, &entity.StockTransferDelivery{
|
||||||
@@ -234,7 +224,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
SupplierId: uint64(delivery.SupplierID),
|
SupplierId: uint64(delivery.SupplierID),
|
||||||
VehiclePlate: delivery.VehiclePlate,
|
VehiclePlate: delivery.VehiclePlate,
|
||||||
DriverName: delivery.DriverName,
|
DriverName: delivery.DriverName,
|
||||||
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses
|
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
|
||||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||||
ShippingCostTotal: delivery.DeliveryCost,
|
ShippingCostTotal: delivery.DeliveryCost,
|
||||||
})
|
})
|
||||||
@@ -243,7 +233,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tambahkan insert ke delivery items sebagai pivot
|
|
||||||
detailMap := make(map[uint64]uint64)
|
detailMap := make(map[uint64]uint64)
|
||||||
for _, d := range details {
|
for _, d := range details {
|
||||||
detailMap[d.ProductId] = d.Id
|
detailMap[d.ProductId] = d.Id
|
||||||
@@ -271,9 +261,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
|
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
|
||||||
|
|
||||||
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
// Kurangi stok di gudang asal
|
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
|
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
|
||||||
@@ -290,15 +278,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||||
|
|
||||||
// create stock log for decrease (source)
|
|
||||||
// beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
// TransactionType: entity.TransactionTypeDecrease,
|
|
||||||
// Quantity: product.ProductQty,
|
|
||||||
// BeforeQuantity: beforeQty,
|
|
||||||
// AfterQuantity: sourcePW.Qty,
|
|
||||||
// LogType: entity.LogTypeTransfer,
|
|
||||||
// LogId: uint(entityTransfer.Id),
|
|
||||||
Decrease: product.ProductQty,
|
Decrease: product.ProductQty,
|
||||||
Notes: "",
|
Notes: "",
|
||||||
LoggableType: entity.LogTypeTransfer,
|
LoggableType: entity.LogTypeTransfer,
|
||||||
@@ -311,7 +291,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambah stok di gudang tujuan
|
|
||||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||||
)
|
)
|
||||||
@@ -320,7 +299,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||||
}
|
}
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,7 +309,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
ProjectFlockKandangId: &projectFlockKandangID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
// CreatedBy: 1, // TODO: should Get from auth middleware
|
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||||
@@ -339,7 +316,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
||||||
}
|
}
|
||||||
// Update stok di gudang tujuan
|
|
||||||
destPW.Quantity += product.ProductQty
|
destPW.Quantity += product.ProductQty
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
|
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
|
||||||
@@ -347,13 +324,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||||
|
|
||||||
// create stock log for increase (destination)
|
|
||||||
// beforeDestQty := destPW.Quantity - product.ProductQty
|
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
// TransactionType: entity.TransactionTypeIncrease,
|
|
||||||
// Quantity: product.ProductQty,
|
|
||||||
// BeforeQuantity: beforeDestQty,
|
|
||||||
// AfterQuantity: destPW.Qty,
|
|
||||||
Increase: product.ProductQty,
|
Increase: product.ProductQty,
|
||||||
LoggableType: entity.LogTypeTransfer,
|
LoggableType: entity.LogTypeTransfer,
|
||||||
LoggableId: uint(entityTransfer.Id),
|
LoggableId: uint(entityTransfer.Id),
|
||||||
@@ -365,7 +336,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Errorf("Failed to create stock log increase: %+v", err)
|
s.Log.Errorf("Failed to create stock log increase: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -376,7 +346,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
|
|
||||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
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"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ type MarketingDeliveryProductRepository interface {
|
|||||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||||
|
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
@@ -74,3 +77,84 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con
|
|||||||
|
|
||||||
return &deliveryProduct, nil
|
return &deliveryProduct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.MarketingDeliveryProduct{}).
|
||||||
|
Preload("MarketingProduct", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("Marketing").
|
||||||
|
Preload("Marketing.Customer").
|
||||||
|
Preload("Marketing.SalesPerson").
|
||||||
|
Preload("ProductWarehouse").
|
||||||
|
Preload("ProductWarehouse.Product").
|
||||||
|
Preload("ProductWarehouse.Warehouse")
|
||||||
|
}).
|
||||||
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
|
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
|
||||||
|
|
||||||
|
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 {
|
||||||
|
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.ProductId > 0 {
|
||||||
|
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.WarehouseId > 0 {
|
||||||
|
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.Search != "" {
|
||||||
|
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?",
|
||||||
|
"%"+filters.Search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.CustomerId > 0 {
|
||||||
|
db = db.Where("marketings.customer_id = ?", filters.CustomerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.SalesPersonId > 0 {
|
||||||
|
db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.MarketingId > 0 {
|
||||||
|
db = db.Where("marketings.id = ?", filters.MarketingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.ProductId > 0 {
|
||||||
|
db = db.Where("product_warehouses.product_id = ?", filters.ProductId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.WarehouseId > 0 {
|
||||||
|
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.ProjectFlockKandangId > 0 {
|
||||||
|
db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filters.DeliveryDate != "" {
|
||||||
|
if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil {
|
||||||
|
nextDate := deliveryDate.AddDate(0, 0, 1)
|
||||||
|
db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.
|
||||||
|
Offset(offset).
|
||||||
|
Limit(limit).
|
||||||
|
Order("marketing_delivery_products.id DESC").
|
||||||
|
Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, total, nil
|
||||||
|
}
|
||||||
|
|||||||
+11
-2
@@ -101,13 +101,22 @@ func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detail, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, result.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
detailDTO := dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*detail, availableQtys, productWarehouses)
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Status closing kandang diperbarui",
|
Message: "Status closing kandang diperbarui",
|
||||||
// Data: dto.ProjectFlockKandangDetailDTO(*result),
|
Data: fiber.Map{
|
||||||
Data: result,
|
"detail": detailDTO,
|
||||||
|
"approval": detailDTO.Approval,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-10
@@ -432,16 +432,30 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
|
|||||||
}
|
}
|
||||||
if s.ApprovalSvc != nil {
|
if s.ApprovalSvc != nil {
|
||||||
closeAction := entity.ApprovalActionApproved
|
closeAction := entity.ApprovalActionApproved
|
||||||
if _, aerr := s.ApprovalSvc.CreateApproval(
|
// Hindari duplikasi jika approval terakhir sudah Closed + Approved
|
||||||
c.Context(),
|
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
if lerr != nil {
|
||||||
id,
|
return nil, lerr
|
||||||
utils.ProjectFlockKandangStepClosed,
|
}
|
||||||
&closeAction,
|
shouldCreate := true
|
||||||
actorID,
|
if latestPFK != nil &&
|
||||||
nil,
|
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepClosed) &&
|
||||||
); aerr != nil {
|
latestPFK.Action != nil && *latestPFK.Action == closeAction {
|
||||||
return nil, aerr
|
shouldCreate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreate {
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
id,
|
||||||
|
utils.ProjectFlockKandangStepClosed,
|
||||||
|
&closeAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
|
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
|
||||||
@@ -499,6 +513,34 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
reopenAction := entity.ApprovalActionUpdated
|
||||||
|
// Hindari duplikasi jika approval terakhir sudah Disetujui + Updated
|
||||||
|
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||||
|
if lerr != nil {
|
||||||
|
return nil, lerr
|
||||||
|
}
|
||||||
|
shouldCreate := true
|
||||||
|
if latestPFK != nil &&
|
||||||
|
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) &&
|
||||||
|
latestPFK.Action != nil && *latestPFK.Action == reopenAction {
|
||||||
|
shouldCreate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreate {
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
id,
|
||||||
|
utils.ProjectFlockKandangStepDisetujui,
|
||||||
|
&reopenAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||||
dtoResult.AvailableQuantity = float64(availableStock)
|
dtoResult.AvailableQuantity = float64(availableStock)
|
||||||
|
|
||||||
// populate available quantity for each kandang inside project_flock
|
|
||||||
if dtoResult.ProjectFlock != nil {
|
if dtoResult.ProjectFlock != nil {
|
||||||
for i := range dtoResult.ProjectFlock.Kandangs {
|
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||||
kand := &dtoResult.ProjectFlock.Kandangs[i]
|
kand := &dtoResult.ProjectFlock.Kandangs[i]
|
||||||
@@ -292,7 +291,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
kand.AvailableQuantity = q
|
kand.AvailableQuantity = q
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remove inner kandangs from project_flock to avoid duplication
|
|
||||||
dtoResult.ProjectFlock.Kandangs = nil
|
dtoResult.ProjectFlock.Kandangs = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
|
|||||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
|
DB() *gorm.DB
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +118,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
|
|||||||
AND "approvals"."approvable_type" = ?
|
AND "approvals"."approvable_type" = ?
|
||||||
AND LOWER("approvals"."step_name") = LOWER(?)
|
AND LOWER("approvals"."step_name") = LOWER(?)
|
||||||
AND "approvals"."id" IN (
|
AND "approvals"."id" IN (
|
||||||
SELECT "id" FROM "approvals"
|
SELECT "approvals"."id" FROM "approvals"
|
||||||
WHERE "approvable_id" = "project_flock_kandangs"."id"
|
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
|
||||||
AND "approvable_type" = ?
|
AND "approvals"."approvable_type" = ?
|
||||||
ORDER BY "action_at" DESC
|
ORDER BY "approvals"."id" DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -238,9 +239,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
|||||||
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||||
latestApprovalSubQuery := r.db.
|
latestApprovalSubQuery := r.db.
|
||||||
Table("approvals").
|
Table("approvals").
|
||||||
Select("DISTINCT ON (approvable_id) approvable_id, step_name, action_at").
|
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
|
||||||
Where("approvable_type = ?", "PROJECT_FLOCKS").
|
Where("approvable_type = ?", "PROJECT_FLOCKS").
|
||||||
Order("approvable_id, action_at DESC")
|
Order("approvable_id, id DESC")
|
||||||
|
|
||||||
var pfkID uint
|
var pfkID uint
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -189,9 +189,6 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
|||||||
if upd.VehicleNumber != nil {
|
if upd.VehicleNumber != nil {
|
||||||
data["vehicle_number"] = upd.VehicleNumber
|
data["vehicle_number"] = upd.VehicleNumber
|
||||||
}
|
}
|
||||||
if upd.ReceivedQty != nil {
|
|
||||||
data["total_qty"] = upd.ReceivedQty
|
|
||||||
}
|
|
||||||
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
|
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
|
||||||
data["warehouse_id"] = upd.WarehouseID
|
data["warehouse_id"] = upd.WarehouseID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -814,7 +814,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
|
|
||||||
// Always ensure PW when qty > 0 so stockable has target.
|
// Always ensure PW when qty > 0 so stockable has target.
|
||||||
if prep.receivedQty > 0 {
|
if prep.receivedQty > 0 {
|
||||||
pwID, err := pwRepoTx.EnsureProductWarehouse(c.Context(), uint(item.ProductId), prep.warehouseID, purchase.CreatedBy)
|
pwID, err := pwRepoTx.EnsureProductWarehouse(
|
||||||
|
c.Context(),
|
||||||
|
uint(item.ProductId),
|
||||||
|
prep.warehouseID,
|
||||||
|
item.ProjectFlockKandangId,
|
||||||
|
purchase.CreatedBy,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
@@ -22,27 +21,35 @@ func NewRepportController(repportService service.RepportService) *RepportControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RepportController) GetAll(ctx *fiber.Ctx) error {
|
func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.ExpenseQuery{
|
||||||
Page: ctx.QueryInt("page", 1),
|
Page: ctx.QueryInt("page", 1),
|
||||||
Limit: ctx.QueryInt("limit", 10),
|
Limit: ctx.QueryInt("limit", 10),
|
||||||
Search: ctx.Query("search", ""),
|
Search: ctx.Query("search", ""),
|
||||||
|
Category: ctx.Query("category", ""),
|
||||||
|
SupplierId: int64(ctx.QueryInt("supplier_id", 0)),
|
||||||
|
KandangId: int64(ctx.QueryInt("kandang_id", 0)),
|
||||||
|
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
NonstockId: int64(ctx.QueryInt("nonstock_id", 0)),
|
||||||
|
AreaId: int64(ctx.QueryInt("area_id", 0)),
|
||||||
|
LocationId: int64(ctx.QueryInt("location_id", 0)),
|
||||||
|
RealizationDate: ctx.Query("realization_date", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := c.RepportService.GetAll(ctx, query)
|
result, totalResults, err := c.RepportService.GetExpense(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).
|
return ctx.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.RepportListDTO]{
|
JSON(response.SuccessWithPaginate[dto.RepportExpenseListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get all reports successfully",
|
Message: "Get expense report successfully",
|
||||||
Meta: response.Meta{
|
Meta: response.Meta{
|
||||||
Page: query.Page,
|
Page: query.Page,
|
||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
@@ -53,46 +60,40 @@ func (c *RepportController) GetAll(ctx *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RepportController) GetOne(ctx *fiber.Ctx) error {
|
func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
||||||
param := ctx.Params("id")
|
query := &validation.MarketingQuery{
|
||||||
|
Page: ctx.QueryInt("page", 1),
|
||||||
id, err := strconv.Atoi(param)
|
Limit: ctx.QueryInt("limit", 10),
|
||||||
if err != nil {
|
Search: ctx.Query("search", ""),
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
CustomerId: int64(ctx.QueryInt("customer_id", 0)),
|
||||||
|
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
DeliveryDate: ctx.Query("delivery_date", ""),
|
||||||
|
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||||
|
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||||
|
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||||
|
MarketingId: int64(ctx.QueryInt("marketing_id", 0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := c.RepportService.GetOne(ctx, uint(id))
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := c.RepportService.GetMarketing(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).
|
return ctx.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get report successfully",
|
Message: "Get marketing report successfully",
|
||||||
Data: result,
|
Meta: response.Meta{
|
||||||
})
|
Page: query.Page,
|
||||||
}
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
|
TotalResults: totalResults,
|
||||||
param := ctx.Params("id")
|
},
|
||||||
|
Data: result,
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := c.RepportService.GetOne(ctx, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Get report successfully",
|
|
||||||
Data: result,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// === DTO Structs ===
|
|
||||||
|
|
||||||
type RepportListDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RepportDetailDTO struct {
|
|
||||||
RepportListDTO
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
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"
|
||||||
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type RepportExpenseBaseDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
PoNumber string `json:"po_number"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
||||||
|
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
||||||
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportExpensePengajuanDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
ExpenseId *uint64 `json:"expense_id,omitempty"`
|
||||||
|
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportExpenseRealisasiDTO struct {
|
||||||
|
Id *uint64 `json:"id,omitempty"`
|
||||||
|
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportExpenseListDTO struct {
|
||||||
|
RepportExpenseBaseDTO
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
|
||||||
|
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
|
||||||
|
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||||
|
TotalRealisasi float64 `json:"total_realisasi"`
|
||||||
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MAPPERS ===
|
||||||
|
|
||||||
|
func ToRepportExpenseBaseDTO(e *entity.Expense) RepportExpenseBaseDTO {
|
||||||
|
var realizationDate *time.Time
|
||||||
|
if !e.RealizationDate.IsZero() {
|
||||||
|
realizationDate = &e.RealizationDate
|
||||||
|
}
|
||||||
|
|
||||||
|
var supplier *supplierDTO.SupplierRelationDTO
|
||||||
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
|
mapped := supplierDTO.ToSupplierRelationDTO(*e.Supplier)
|
||||||
|
supplier = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportExpenseBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
ReferenceNumber: e.ReferenceNumber,
|
||||||
|
PoNumber: e.PoNumber,
|
||||||
|
Category: e.Category,
|
||||||
|
Supplier: supplier,
|
||||||
|
RealizationDate: realizationDate,
|
||||||
|
TransactionDate: e.TransactionDate,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportExpensePengajuanDTO(ns *entity.ExpenseNonstock) RepportExpensePengajuanDTO {
|
||||||
|
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||||
|
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||||
|
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
||||||
|
nonstock = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportExpensePengajuanDTO{
|
||||||
|
Id: ns.Id,
|
||||||
|
ExpenseId: ns.ExpenseId,
|
||||||
|
ProjectFlockKandangId: ns.ProjectFlockKandangId,
|
||||||
|
Qty: ns.Qty,
|
||||||
|
Price: ns.Price,
|
||||||
|
Notes: ns.Notes,
|
||||||
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: ns.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportExpenseRealisasiDTO(r *entity.ExpenseRealization) RepportExpenseRealisasiDTO {
|
||||||
|
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||||
|
if r.ExpenseNonstock != nil && r.ExpenseNonstock.Nonstock != nil && r.ExpenseNonstock.Nonstock.Id != 0 {
|
||||||
|
mapped := nonstockDTO.ToNonstockRelationDTO(*r.ExpenseNonstock.Nonstock)
|
||||||
|
nonstock = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportExpenseRealisasiDTO{
|
||||||
|
Id: r.ExpenseNonstockId,
|
||||||
|
ExpenseNonstockId: r.ExpenseNonstockId,
|
||||||
|
Qty: r.Qty,
|
||||||
|
Price: r.Price,
|
||||||
|
Notes: r.Notes,
|
||||||
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNonstock, latestApproval *approvalDTO.ApprovalRelationDTO) RepportExpenseListDTO {
|
||||||
|
var realisasi RepportExpenseRealisasiDTO
|
||||||
|
if ns.Realization != nil {
|
||||||
|
realisasi = ToRepportExpenseRealisasiDTO(ns.Realization)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPengajuan := ns.Qty * ns.Price
|
||||||
|
totalRealisasi := float64(0)
|
||||||
|
if ns.Realization != nil {
|
||||||
|
totalRealisasi = ns.Realization.Qty * ns.Realization.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get kandang data at the main level
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if ns.Kandang != nil && ns.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(*ns.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportExpenseListDTO{
|
||||||
|
RepportExpenseBaseDTO: baseDTO,
|
||||||
|
Kandang: kandang,
|
||||||
|
Pengajuan: ToRepportExpensePengajuanDTO(ns),
|
||||||
|
Realisasi: realisasi,
|
||||||
|
TotalPengajuan: totalPengajuan,
|
||||||
|
TotalRealisasi: totalRealisasi,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportExpenseListDTOs(realizations []entity.ExpenseRealization) []RepportExpenseListDTO {
|
||||||
|
result := make([]RepportExpenseListDTO, 0, len(realizations))
|
||||||
|
|
||||||
|
for _, realization := range realizations {
|
||||||
|
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Expense == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expense := realization.ExpenseNonstock.Expense
|
||||||
|
baseDTO := ToRepportExpenseBaseDTO(expense)
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||||
|
if expense.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*expense.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary realization with the current realization data
|
||||||
|
if realization.ExpenseNonstock.Realization == nil {
|
||||||
|
realization.ExpenseNonstock.Realization = &realization
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := ToRepportExpenseListDTO(baseDTO, realization.ExpenseNonstock, latestApproval)
|
||||||
|
result = append(result, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type RepportMarketingBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
SoNumber string `json:"so_number"`
|
||||||
|
SoDate time.Time `json:"so_date"`
|
||||||
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
|
SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportMarketingProductDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
MarketingProductId uint `json:"marketing_product_id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportMarketingDeliveryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
MarketingProductId uint `json:"marketing_product_id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalWeight float64 `json:"total_weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
DeliveryDate *time.Time `json:"delivery_date,omitempty"`
|
||||||
|
VehicleNumber string `json:"vehicle_number"`
|
||||||
|
DoNumber string `json:"do_number"`
|
||||||
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportMarketingListDTO struct {
|
||||||
|
RepportMarketingBaseDTO
|
||||||
|
MarketingProduct RepportMarketingProductDTO `json:"marketing_product"`
|
||||||
|
MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"`
|
||||||
|
TotalMarketingProduct float64 `json:"total_marketing_product"`
|
||||||
|
TotalMarketingDelivery float64 `json:"total_marketing_delivery"`
|
||||||
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MAPPERS ===
|
||||||
|
|
||||||
|
func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO {
|
||||||
|
if m == nil {
|
||||||
|
return RepportMarketingBaseDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerRelationDTO
|
||||||
|
if m.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerRelationDTO(m.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var salesPerson *userDTO.UserRelationDTO
|
||||||
|
if m.SalesPerson.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(m.SalesPerson)
|
||||||
|
salesPerson = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportMarketingBaseDTO{
|
||||||
|
Id: m.Id,
|
||||||
|
SoNumber: m.SoNumber,
|
||||||
|
SoDate: m.SoDate,
|
||||||
|
Customer: customer,
|
||||||
|
SalesPerson: salesPerson,
|
||||||
|
Notes: m.Notes,
|
||||||
|
CreatedAt: m.CreatedAt,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO {
|
||||||
|
if mp == nil {
|
||||||
|
return RepportMarketingProductDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var product *productDTO.ProductRelationDTO
|
||||||
|
if mp.ProductWarehouse.Product.Id != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product)
|
||||||
|
product = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportMarketingProductDTO{
|
||||||
|
Id: mp.Id,
|
||||||
|
MarketingProductId: mp.Id,
|
||||||
|
Qty: mp.Qty,
|
||||||
|
UnitPrice: mp.UnitPrice,
|
||||||
|
AvgWeight: mp.AvgWeight,
|
||||||
|
TotalWeight: mp.TotalWeight,
|
||||||
|
TotalPrice: mp.TotalPrice,
|
||||||
|
Product: product,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO {
|
||||||
|
if mdp == nil {
|
||||||
|
return RepportMarketingDeliveryDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var product *productDTO.ProductRelationDTO
|
||||||
|
if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||||
|
product = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseId := uint(0)
|
||||||
|
if mdp.MarketingProduct.ProductWarehouse.Id != 0 {
|
||||||
|
warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId
|
||||||
|
}
|
||||||
|
|
||||||
|
doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId)
|
||||||
|
|
||||||
|
return RepportMarketingDeliveryDTO{
|
||||||
|
Id: mdp.Id,
|
||||||
|
MarketingProductId: mdp.MarketingProductId,
|
||||||
|
Qty: mdp.Qty,
|
||||||
|
UnitPrice: mdp.UnitPrice,
|
||||||
|
TotalWeight: mdp.TotalWeight,
|
||||||
|
AvgWeight: mdp.AvgWeight,
|
||||||
|
TotalPrice: mdp.TotalPrice,
|
||||||
|
DeliveryDate: mdp.DeliveryDate,
|
||||||
|
VehicleNumber: mdp.VehicleNumber,
|
||||||
|
DoNumber: doNumber,
|
||||||
|
Product: product,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO {
|
||||||
|
var marketingProduct RepportMarketingProductDTO
|
||||||
|
var marketingDelivery RepportMarketingDeliveryDTO
|
||||||
|
|
||||||
|
if mp != nil {
|
||||||
|
marketingProduct = ToRepportMarketingProductDTO(mp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp != nil {
|
||||||
|
marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalMarketingProduct := float64(0)
|
||||||
|
totalMarketingDelivery := float64(0)
|
||||||
|
|
||||||
|
if mp != nil {
|
||||||
|
totalMarketingProduct = mp.Qty * mp.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
if mdp != nil {
|
||||||
|
totalMarketingDelivery = mdp.Qty * mdp.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
return RepportMarketingListDTO{
|
||||||
|
RepportMarketingBaseDTO: baseDTO,
|
||||||
|
MarketingProduct: marketingProduct,
|
||||||
|
MarketingDelivery: marketingDelivery,
|
||||||
|
TotalMarketingProduct: totalMarketingProduct,
|
||||||
|
TotalMarketingDelivery: totalMarketingDelivery,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO {
|
||||||
|
result := make([]RepportMarketingListDTO, 0, len(deliveryProducts))
|
||||||
|
|
||||||
|
marketingMap := make(map[uint]entity.MarketingDeliveryProduct)
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
if dp.MarketingProduct.Marketing.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
marketingID := dp.MarketingProduct.Marketing.Id
|
||||||
|
if _, exists := marketingMap[marketingID]; !exists {
|
||||||
|
marketingMap[marketingID] = dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, deliveryProduct := range marketingMap {
|
||||||
|
if deliveryProduct.MarketingProduct.Marketing.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
marketing := &deliveryProduct.MarketingProduct.Marketing
|
||||||
|
baseDTO := ToRepportMarketingBaseDTO(marketing)
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||||
|
if marketing.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
mdp := &deliveryProduct
|
||||||
|
dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval)
|
||||||
|
result = append(result, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -5,19 +5,24 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
|
|
||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RepportModule struct{}
|
type RepportModule struct{}
|
||||||
|
|
||||||
func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
// Initialize expense realization repository
|
|
||||||
expRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
|
||||||
|
|
||||||
// Initialize report service with expense realization repo
|
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
|
||||||
repportService := sRepport.NewRepportService(validate, expRealizationRepo)
|
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
|
||||||
|
approvalRepository := commonRepo.NewApprovalRepository(db)
|
||||||
|
|
||||||
|
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
||||||
|
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc)
|
||||||
|
|
||||||
RepportRoutes(router, repportService)
|
RepportRoutes(router, repportService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package repports
|
package repports
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers"
|
||||||
repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
|
|
||||||
@@ -11,10 +10,8 @@ import (
|
|||||||
func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
|
func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
|
||||||
ctrl := controller.NewRepportController(s)
|
ctrl := controller.NewRepportController(s)
|
||||||
|
|
||||||
route := v1.Group("/repports")
|
route := v1.Group("/reports")
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/expense", ctrl.GetExpense)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/marketing", ctrl.GetMarketing)
|
||||||
|
|
||||||
route.Get("expense", ctrl.GetExpense)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +1,115 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RepportService interface {
|
type RepportService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error)
|
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
|
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error)
|
||||||
GetExpense(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type repportService struct {
|
type repportService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
dummyData map[uint]dto.RepportListDTO
|
|
||||||
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
||||||
|
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
|
ApprovalSvc approvalService.ApprovalService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository) RepportService {
|
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService {
|
||||||
// Initialize with dummy data
|
|
||||||
now := time.Now()
|
|
||||||
dummyData := map[uint]dto.RepportListDTO{
|
|
||||||
1: {
|
|
||||||
Id: 1,
|
|
||||||
Name: "Sales Report",
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
Id: 2,
|
|
||||||
Name: "Inventory Report",
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
Id: 3,
|
|
||||||
Name: "Production Report",
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &repportService{
|
return &repportService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
dummyData: dummyData,
|
|
||||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
MarketingDeliveryRepo: marketingDeliveryRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error) {
|
func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert map to slice
|
|
||||||
var results []dto.RepportListDTO
|
|
||||||
for _, v := range s.dummyData {
|
|
||||||
// Apply search filter if provided
|
|
||||||
if params.Search != "" && !strings.Contains(strings.ToLower(v.Name), strings.ToLower(params.Search)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results = append(results, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply pagination
|
|
||||||
total := int64(len(results))
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
if offset >= int(total) {
|
realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||||
return []dto.RepportListDTO{}, total, nil
|
if err != nil {
|
||||||
|
s.Log.Errorf("GetAllWithFilters error: %v", err)
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
end := offset + params.Limit
|
result := dto.ToRepportExpenseListDTOs(realizations)
|
||||||
if end > int(total) {
|
|
||||||
end = int(total)
|
expenseIDs := make([]uint, 0, len(result))
|
||||||
|
for i := range result {
|
||||||
|
expenseIDs = append(expenseIDs, uint(result[i].Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
return results[offset:end], total, nil
|
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("LatestByTargets error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
expenseIDAsUint := uint(result[i].Id)
|
||||||
|
if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*approval)
|
||||||
|
result[i].LatestApproval = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) GetOne(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
|
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) {
|
||||||
if data, ok := s.dummyData[id]; ok {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return &data, nil
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *repportService) GetExpense(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
|
offset := (params.Page - 1) * params.Limit
|
||||||
if data, ok := s.dummyData[id]; ok {
|
|
||||||
return &data, nil
|
deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
|
|
||||||
|
marketingIDMap := make(map[uint]bool)
|
||||||
|
marketingIDs := make([]uint, 0)
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] {
|
||||||
|
marketingIDs = append(marketingIDs, marketingID)
|
||||||
|
marketingIDMap[marketingID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("LatestByTargets error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range deliveryProducts {
|
||||||
|
if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil {
|
||||||
|
deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,29 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Query struct {
|
type ExpenseQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
|
Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
|
SupplierId int64 `query:"supplier_id" validate:"omitempty"`
|
||||||
|
KandangId int64 `query:"kandang_id" validate:"omitempty"`
|
||||||
|
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"`
|
||||||
|
ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"`
|
||||||
|
NonstockId int64 `query:"nonstock_id" validate:"omitempty"`
|
||||||
|
AreaId int64 `query:"area_id" validate:"omitempty"`
|
||||||
|
LocationId int64 `query:"location_id" validate:"omitempty"`
|
||||||
|
RealizationDate string `query:"realization_date" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingQuery struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
|
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
||||||
|
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"`
|
||||||
|
DeliveryDate string `query:"delivery_date" validate:"omitempty"`
|
||||||
|
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||||
|
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||||
|
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||||
|
MarketingId int64 `query:"marketing_id" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user