mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat/BE/US-284/TASK-,299-Create API (GET ONE in tab Perhitungan Sapronak)
This commit is contained in:
@@ -14,11 +14,15 @@ import (
|
||||
|
||||
type ClosingController struct {
|
||||
ClosingService service.ClosingService
|
||||
SapronakService service.SapronakService
|
||||
SapronakFormatter service.SapronakFormatter
|
||||
}
|
||||
|
||||
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService, sapronakFormatter service.SapronakFormatter) *ClosingController {
|
||||
return &ClosingController{
|
||||
ClosingService: closingService,
|
||||
SapronakService: sapronakService,
|
||||
SapronakFormatter: sapronakFormatter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +127,56 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := u.SapronakFormatter.ProjectPayload(result)
|
||||
|
||||
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")
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := u.SapronakFormatter.KandangPayload(result)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get perhitungan sapronak per kandang successfully",
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package dto
|
||||
|
||||
import "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"`
|
||||
Ovk SapronakCategoryDTO `json:"ovk"`
|
||||
Pakan SapronakCategoryDTO `json:"pakan"`
|
||||
}
|
||||
@@ -28,7 +28,8 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ClosingRoutes(router, userService, closingService)
|
||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ClosingRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlock]
|
||||
ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error)
|
||||
MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error)
|
||||
FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
}
|
||||
|
||||
type ClosingRepositoryImpl struct {
|
||||
@@ -19,3 +33,398 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
||||
}
|
||||
}
|
||||
|
||||
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) ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
db := r.DB().
|
||||
WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("Kandang")
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return pfks, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error) {
|
||||
result := make(map[uint]time.Time, len(pfkIDs))
|
||||
if len(pfkIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
||||
StartDate *time.Time `gorm:"column:start_date"`
|
||||
}
|
||||
|
||||
if err := r.DB().
|
||||
WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("project_flock_kandang_id, MIN(chick_in_date) AS start_date").
|
||||
Where("project_flock_kandang_id IN ?", pfkIDs).
|
||||
Group("project_flock_kandang_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row.StartDate != nil {
|
||||
result[row.ProjectFlockKandangID] = row.StartDate.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error) {
|
||||
rows := make([]SapronakIncomingRow, 0)
|
||||
|
||||
db := r.DB().
|
||||
WithContext(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
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
|
||||
`).
|
||||
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 ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)}).
|
||||
Where("pi.received_date IS NOT NULL")
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("pi.received_date >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("pi.received_date < ?", *end)
|
||||
}
|
||||
|
||||
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) FetchSapronakUsage(ctx context.Context, pfkID uint, start, end *time.Time) ([]SapronakUsageRow, error) {
|
||||
rows := make([]SapronakUsageRow, 0)
|
||||
if pfkID == 0 {
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
db := r.DB().
|
||||
WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(rs.usage_qty), 0) AS qty,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`).
|
||||
Joins("JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.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).
|
||||
Where("r.project_flock_kandangs_id = ?", pfkID).
|
||||
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("r.record_datetime >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("r.record_datetime < ?", *end)
|
||||
}
|
||||
|
||||
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) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
|
||||
rows := make([]SapronakDetailRow, 0)
|
||||
|
||||
db := r.DB().
|
||||
WithContext(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
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
|
||||
`).
|
||||
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 ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)}).
|
||||
Where("pi.received_date IS NOT NULL")
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("pi.received_date >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("pi.received_date < ?", *end)
|
||||
}
|
||||
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint][]SapronakDetailRow)
|
||||
for _, row := range rows {
|
||||
result[row.ProductID] = append(result[row.ProductID], row)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error) {
|
||||
rows := make([]SapronakDetailRow, 0)
|
||||
|
||||
db := r.DB().
|
||||
WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select(`
|
||||
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
|
||||
`).
|
||||
Joins("JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = rs.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).
|
||||
Where("r.project_flock_kandangs_id = ?", pfkID).
|
||||
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("r.record_datetime >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("r.record_datetime < ?", *end)
|
||||
}
|
||||
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint][]SapronakDetailRow)
|
||||
for _, row := range rows {
|
||||
result[row.ProductID] = append(result[row.ProductID], row)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
incoming := make(map[uint][]SapronakDetailRow)
|
||||
outgoing := make(map[uint][]SapronakDetailRow)
|
||||
|
||||
rows := make([]struct {
|
||||
ID uint
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
CreatedAt *time.Time
|
||||
Increase float64
|
||||
Decrease float64
|
||||
Price float64
|
||||
}, 0)
|
||||
|
||||
db := r.DB().
|
||||
WithContext(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
|
||||
`).
|
||||
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").
|
||||
Where("sl.loggable_type = ?", entity.LogTypeAdjustment).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("sl.created_at >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("sl.created_at < ?", *end)
|
||||
}
|
||||
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
ref := fmt.Sprintf("ADJ-%d", row.ID)
|
||||
if row.Increase > 0 {
|
||||
incoming[row.ProductID] = append(incoming[row.ProductID], SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: ref,
|
||||
QtyIn: row.Increase,
|
||||
QtyOut: 0,
|
||||
Price: row.Price,
|
||||
})
|
||||
}
|
||||
if row.Decrease > 0 {
|
||||
outgoing[row.ProductID] = append(outgoing[row.ProductID], SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: ref,
|
||||
QtyIn: 0,
|
||||
QtyOut: row.Decrease,
|
||||
Price: row.Price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return incoming, outgoing, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
incoming := make(map[uint][]SapronakDetailRow)
|
||||
outgoing := make(map[uint][]SapronakDetailRow)
|
||||
|
||||
rows := make([]struct {
|
||||
ID uint
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
CreatedAt *time.Time
|
||||
Increase float64
|
||||
Decrease float64
|
||||
Price float64
|
||||
}, 0)
|
||||
|
||||
db := r.DB().
|
||||
WithContext(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
|
||||
`).
|
||||
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").
|
||||
Where("sl.loggable_type = ?", entity.LogTypeTransfer).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", []string{string(utils.FlagDOC), string(utils.FlagPakan), string(utils.FlagOVK)})
|
||||
|
||||
if start != nil {
|
||||
db = db.Where("sl.created_at >= ?", *start)
|
||||
}
|
||||
if end != nil {
|
||||
db = db.Where("sl.created_at < ?", *end)
|
||||
}
|
||||
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
ref := fmt.Sprintf("TRF-%d", row.ID)
|
||||
if row.Increase > 0 {
|
||||
incoming[row.ProductID] = append(incoming[row.ProductID], SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: ref,
|
||||
QtyIn: row.Increase,
|
||||
QtyOut: 0,
|
||||
Price: row.Price,
|
||||
})
|
||||
}
|
||||
if row.Decrease > 0 {
|
||||
outgoing[row.ProductID] = append(outgoing[row.ProductID], SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: ref,
|
||||
QtyIn: 0,
|
||||
QtyOut: row.Decrease,
|
||||
Price: row.Price,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return incoming, outgoing, nil
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
||||
ctrl := controller.NewClosingController(s)
|
||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
|
||||
formatter := closing.NewSapronakFormatter()
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc, formatter)
|
||||
|
||||
route := v1.Group("/closing")
|
||||
|
||||
@@ -22,5 +23,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
minStep = rec.StepNumber
|
||||
statusProject = rec.StepName
|
||||
}
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
|
||||
if rec.StepNumber == uint16(utils.ProjectFlockStepAktif) {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
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"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type SapronakService interface {
|
||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint) ([]dto.SapronakReportDTO, error)
|
||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint) (*dto.SapronakReportDTO, error)
|
||||
GetSapronakReport(ctx *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error)
|
||||
}
|
||||
|
||||
type sapronakService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
}
|
||||
|
||||
func NewSapronakService(repo repository.ClosingRepository, validate *validator.Validate) SapronakService {
|
||||
return &sapronakService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.computeSapronakReports(c.Context(), params)
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint) ([]dto.SapronakReportDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||
}
|
||||
reports, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
Status: "all",
|
||||
})
|
||||
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) (*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.SapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
ProjectFlockKandangID: pfkID,
|
||||
Status: "all",
|
||||
})
|
||||
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.SapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pfks) == 0 {
|
||||
return []dto.SapronakReportDTO{}, nil
|
||||
}
|
||||
|
||||
startMap, err := s.mapStartDates(ctx, pfks)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to prepare start dates for sapronak report: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare sapronak report")
|
||||
}
|
||||
statusMap, nextStartMap := s.computeStatusAndNextStart(pfks, startMap)
|
||||
|
||||
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||
if filterStatus == "" {
|
||||
filterStatus = "all"
|
||||
}
|
||||
|
||||
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||
for _, pfk := range pfks {
|
||||
status := statusMap[pfk.Id]
|
||||
if status == "" {
|
||||
status = "closing"
|
||||
}
|
||||
|
||||
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||
continue
|
||||
}
|
||||
|
||||
start := startMap[pfk.Id]
|
||||
var startPtr *time.Time
|
||||
if !start.IsZero() {
|
||||
startCopy := start
|
||||
startPtr = &startCopy
|
||||
}
|
||||
|
||||
var endPtr *time.Time
|
||||
if end, ok := nextStartMap[pfk.Id]; ok {
|
||||
endCopy := end
|
||||
endPtr = &endCopy
|
||||
}
|
||||
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, startPtr, endPtr)
|
||||
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: startPtr,
|
||||
EndDate: endPtr,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
pfks, err := s.Repository.ListProjectFlockKandangsForSapronak(ctx, params)
|
||||
if 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) mapStartDates(ctx context.Context, pfks []entity.ProjectFlockKandang) (map[uint]time.Time, error) {
|
||||
result := make(map[uint]time.Time, len(pfks))
|
||||
if len(pfks) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
ids := make([]uint, len(pfks))
|
||||
for i, pfk := range pfks {
|
||||
ids[i] = pfk.Id
|
||||
}
|
||||
|
||||
startDates, err := s.Repository.MapSapronakStartDates(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pfk := range pfks {
|
||||
if start, ok := startDates[pfk.Id]; ok {
|
||||
result[pfk.Id] = start
|
||||
continue
|
||||
}
|
||||
result[pfk.Id] = pfk.CreatedAt.UTC()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||
if len(reports) == 0 {
|
||||
return dto.SapronakReportDTO{}
|
||||
}
|
||||
|
||||
var (
|
||||
totalIncoming float64
|
||||
totalUsage float64
|
||||
earliestStart *time.Time
|
||||
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
|
||||
if r.StartDate != nil {
|
||||
if earliestStart == nil || r.StartDate.Before(*earliestStart) {
|
||||
earliestStart = r.StartDate
|
||||
}
|
||||
}
|
||||
|
||||
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: earliestStart,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func (s sapronakService) computeStatusAndNextStart(pfks []entity.ProjectFlockKandang, startMap map[uint]time.Time) (map[uint]string, map[uint]time.Time) {
|
||||
statusMap := make(map[uint]string, len(pfks))
|
||||
nextStartMap := make(map[uint]time.Time, len(pfks))
|
||||
|
||||
if len(pfks) == 0 {
|
||||
return statusMap, nextStartMap
|
||||
}
|
||||
|
||||
grouped := make(map[uint][]entity.ProjectFlockKandang)
|
||||
for _, pfk := range pfks {
|
||||
grouped[pfk.KandangId] = append(grouped[pfk.KandangId], pfk)
|
||||
}
|
||||
|
||||
for _, list := range grouped {
|
||||
for idx, item := range list {
|
||||
if idx < len(list)-1 {
|
||||
next := list[idx+1]
|
||||
if start, ok := startMap[next.Id]; ok {
|
||||
nextStartMap[item.Id] = start
|
||||
}
|
||||
statusMap[item.Id] = "closing"
|
||||
continue
|
||||
}
|
||||
statusMap[item.Id] = "active"
|
||||
}
|
||||
}
|
||||
|
||||
return statusMap, nextStartMap
|
||||
}
|
||||
|
||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
transIncomingRows, _, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId, start, end)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
incoming, usage := mapIncomingUsage(incomingRows, usageRows)
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||
details := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows)
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
}
|
||||
|
||||
return items, groups, totalIncoming, totalUsage, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
)
|
||||
|
||||
type SapronakFormatter interface {
|
||||
ProjectPayload(reports []dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO
|
||||
KandangPayload(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO
|
||||
}
|
||||
|
||||
type sapronakFormatter struct{}
|
||||
|
||||
func NewSapronakFormatter() SapronakFormatter {
|
||||
return &sapronakFormatter{}
|
||||
}
|
||||
|
||||
func (f *sapronakFormatter) ProjectPayload(reports []dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO {
|
||||
result := dto.SapronakProjectAggregatedDTO{
|
||||
Doc: dto.SapronakCategoryDTO{},
|
||||
Ovk: dto.SapronakCategoryDTO{},
|
||||
Pakan: dto.SapronakCategoryDTO{},
|
||||
}
|
||||
|
||||
if len(reports) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
rep := reports[0]
|
||||
return f.mapFromReport(&rep)
|
||||
}
|
||||
|
||||
func (f *sapronakFormatter) KandangPayload(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO {
|
||||
return f.mapFromReport(report)
|
||||
}
|
||||
|
||||
func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO {
|
||||
result := dto.SapronakProjectAggregatedDTO{
|
||||
Doc: dto.SapronakCategoryDTO{},
|
||||
Ovk: dto.SapronakCategoryDTO{},
|
||||
Pakan: dto.SapronakCategoryDTO{},
|
||||
}
|
||||
|
||||
if report == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
byFlag := map[string]*dto.SapronakCategoryDTO{
|
||||
"DOC": &result.Doc,
|
||||
"OVK": &result.Ovk,
|
||||
"PAKAN": &result.Pakan,
|
||||
}
|
||||
|
||||
formatDate := func(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("02-Jan-2006")
|
||||
}
|
||||
|
||||
for _, group := range report.Groups {
|
||||
flag := strings.ToUpper(group.Flag)
|
||||
target := byFlag[flag]
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
for idx, item := range group.Items {
|
||||
qtyUsed := item.QtyKeluar
|
||||
if qtyUsed == 0 {
|
||||
qtyUsed = item.QtyMasuk
|
||||
}
|
||||
|
||||
target.Rows = append(target.Rows, dto.SapronakCategoryRowDTO{
|
||||
ID: idx + 1,
|
||||
Date: formatDate(item.Tanggal),
|
||||
ReferenceNumber: item.NoReferensi,
|
||||
QtyIn: item.QtyMasuk,
|
||||
QtyOut: item.QtyKeluar,
|
||||
QtyUsed: qtyUsed,
|
||||
Description: item.ProductName,
|
||||
ProductCategory: item.ProductName,
|
||||
UnitPrice: item.Harga,
|
||||
TotalAmount: item.Nilai,
|
||||
Notes: "-",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
buildTotals := func(cat *dto.SapronakCategoryDTO, label string) {
|
||||
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 = dto.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")
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package validation
|
||||
|
||||
type SapronakQuery 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"`
|
||||
Debug bool `query:"debug"`
|
||||
}
|
||||
Reference in New Issue
Block a user