mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-25 15:55:44 +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 {
|
type ClosingController struct {
|
||||||
ClosingService service.ClosingService
|
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{
|
return &ClosingController{
|
||||||
ClosingService: closingService,
|
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),
|
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)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||||
|
sapronakService := sClosing.NewSapronakService(closingRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService)
|
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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/closings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClosingRepository interface {
|
type ClosingRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
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 {
|
type ClosingRepositoryImpl struct {
|
||||||
@@ -19,3 +33,398 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
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"
|
"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)
|
formatter := closing.NewSapronakFormatter()
|
||||||
|
ctrl := controller.NewClosingController(s, sapronakSvc, formatter)
|
||||||
|
|
||||||
route := v1.Group("/closing")
|
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("/", ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
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)
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,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,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