mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment
This commit is contained in:
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
@@ -13,12 +14,14 @@ import (
|
||||
)
|
||||
|
||||
type ClosingController struct {
|
||||
ClosingService service.ClosingService
|
||||
ClosingService service.ClosingService
|
||||
SapronakService service.SapronakService
|
||||
}
|
||||
|
||||
func NewClosingController(closingService service.ClosingService) *ClosingController {
|
||||
func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
|
||||
return &ClosingController{
|
||||
ClosingService: closingService,
|
||||
ClosingService: closingService,
|
||||
SapronakService: sapronakService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,17 +42,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all closings successfully",
|
||||
Message: "Retrieved closing projects list successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToClosingListDTOs(result),
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,3 +126,122 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
|
||||
projectFlockID, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||
}
|
||||
|
||||
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get overhead successfully",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||
param := c.Params("projectFlockId")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||
}
|
||||
|
||||
query := &validation.ClosingSapronakQuery{
|
||||
Type: strings.ToLower(c.Query("type")),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Retrieved closing report (sapronak) successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||
param := c.Params("project_flock_id")
|
||||
flag := c.Query("flag", "")
|
||||
|
||||
projectID, err := strconv.Atoi(param)
|
||||
if err != nil || projectID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get perhitungan sapronak per project successfully",
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||
projectParam := c.Params("project_flock_id")
|
||||
kandangParam := c.Params("project_flock_kandang_id")
|
||||
flag := c.Query("flag", "")
|
||||
|
||||
projectID, err := strconv.Atoi(projectParam)
|
||||
if err != nil || projectID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
pfkID, err := strconv.Atoi(kandangParam)
|
||||
if err != nil || pfkID <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get perhitungan sapronak per kandang successfully",
|
||||
Data: payload,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,20 +27,35 @@ type ClosingDetailDTO struct {
|
||||
ClosingListDTO
|
||||
}
|
||||
|
||||
type ClosingListItemDTO struct {
|
||||
Id uint `json:"id"`
|
||||
LocationID uint `json:"location_id"`
|
||||
LocationName string `json:"location_name"`
|
||||
ProjectCategory string `json:"project_category"`
|
||||
Period int `json:"period"`
|
||||
ClosingDate string `json:"closing_date"`
|
||||
ShedLabel string `json:"shed_label"`
|
||||
ShedCount int `json:"shed_count"`
|
||||
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
ProjectStatus string `json:"project_status"`
|
||||
}
|
||||
|
||||
type ClosingSummaryDTO struct {
|
||||
LocationID uint `json:"location_id"`
|
||||
Periode int `json:"periode"`
|
||||
JenisProduk string `json:"jenis_produk"`
|
||||
LabelPopulasi string `json:"label_populasi"`
|
||||
JumlahPopulasi int `json:"jumlah_populasi"`
|
||||
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
|
||||
JenisProject string `json:"jenis_project"`
|
||||
KandangAktif int `json:"kandang_aktif"`
|
||||
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
|
||||
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
|
||||
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||
StatusProject string `json:"status_project"`
|
||||
StatusClosing string `json:"status_closing"`
|
||||
FlockID uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
// JenisProduk string `json:"jenis_produk"`
|
||||
// LabelPopulasi string `json:"label_populasi"`
|
||||
Population int `json:"population"`
|
||||
PopulationFormatted string `json:"population_formatted"`
|
||||
ProjectType string `json:"project_type"`
|
||||
ActiveHouseCount int `json:"active_house_count"`
|
||||
ActiveHouseLabel string `json:"active_house_label"`
|
||||
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||
StatusProject string `json:"project_status"`
|
||||
StatusClosing string `json:"closing_status"`
|
||||
}
|
||||
|
||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||
@@ -52,19 +67,38 @@ func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosi
|
||||
populationInt := int(population)
|
||||
|
||||
return ClosingSummaryDTO{
|
||||
LocationID: project.LocationId,
|
||||
Periode: period,
|
||||
JenisProduk: project.Category,
|
||||
LabelPopulasi: "",
|
||||
JumlahPopulasi: populationInt,
|
||||
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||
JenisProject: "",
|
||||
KandangAktif: kandangCount,
|
||||
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
|
||||
StatusPembayaranPenjualan: "Tempo",
|
||||
StatusPembayaranMitra: "",
|
||||
StatusProject: statusProject,
|
||||
StatusClosing: statusClosing,
|
||||
FlockID: project.Id,
|
||||
Period: period,
|
||||
// JenisProduk: project.Category,
|
||||
// LabelPopulasi: "",
|
||||
Population: populationInt,
|
||||
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||
ProjectType: project.Category,
|
||||
ActiveHouseCount: kandangCount,
|
||||
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
||||
SalesPaymentStatus: "Tempo",
|
||||
// StatusPembayaranMitra: "",
|
||||
StatusProject: statusProject,
|
||||
StatusClosing: statusClosing,
|
||||
}
|
||||
}
|
||||
|
||||
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
||||
shedCount := len(project.KandangHistory)
|
||||
|
||||
return ClosingListItemDTO{
|
||||
Id: project.Id,
|
||||
LocationID: project.LocationId,
|
||||
LocationName: project.Location.Name,
|
||||
ProjectCategory: project.Category,
|
||||
Period: maxPeriod(project.KandangHistory),
|
||||
ClosingDate: "17-Nov-2025",
|
||||
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||
ShedCount: shedCount,
|
||||
SalesPaidAmount: 21993726,
|
||||
SalesRemainingAmount: 11075919,
|
||||
SalesPaymentStatus: "Lunas",
|
||||
ProjectStatus: projectStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||
)
|
||||
|
||||
// === Response DTO ===
|
||||
|
||||
type SalesDTO struct {
|
||||
Id uint `json:"id"`
|
||||
RealizationDate time.Time `json:"realization_date"`
|
||||
@@ -29,10 +28,7 @@ type SalesDTO struct {
|
||||
}
|
||||
|
||||
type PenjualanRealisasiResponseDTO struct {
|
||||
ProjectType string `json:"project_type"`
|
||||
FlockId uint `json:"flock_id"`
|
||||
Period int `json:"period"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
Sales []SalesDTO `json:"sales"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
@@ -88,12 +84,10 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||
}
|
||||
|
||||
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||
period := extractPeriodFromRealisasi(e)
|
||||
|
||||
return PenjualanRealisasiResponseDTO{
|
||||
ProjectType: projectType,
|
||||
FlockId: projectFlockID,
|
||||
Period: period,
|
||||
Sales: ToSalesDTOs(e),
|
||||
|
||||
Sales: ToSalesDTOs(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type OverheadDTO struct {
|
||||
ItemName string `json:"item_name"`
|
||||
UOMName string `json:"uom_name"`
|
||||
BudgetQuantity float64 `json:"budget_quantity"`
|
||||
BudgetUnitPrice float64 `json:"budget_unit_price"`
|
||||
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||
ActualDate string `json:"actual_date"`
|
||||
ActualQuantity float64 `json:"actual_quantity"`
|
||||
ActualUnitPrice float64 `json:"actual_unit_price"`
|
||||
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||
CostPerBird float64 `json:"cost_per_bird"`
|
||||
}
|
||||
|
||||
type TotalDTO struct {
|
||||
BudgetQuantity float64 `json:"budget_quantity"`
|
||||
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||
ActualQuantity float64 `json:"actual_quantity"`
|
||||
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||
CostPerBird float64 `json:"cost_per_bird"`
|
||||
}
|
||||
|
||||
type OverheadListDTO struct {
|
||||
Total TotalDTO `json:"total"`
|
||||
Overheads []OverheadDTO `json:"overheads"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
|
||||
if budget == nil && realization == nil {
|
||||
return OverheadDTO{}
|
||||
}
|
||||
|
||||
var itemName, itemUOM string
|
||||
if budget != nil {
|
||||
itemName, itemUOM = getItemInfo(budget.Nonstock)
|
||||
}
|
||||
|
||||
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
|
||||
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
|
||||
}
|
||||
|
||||
dto := OverheadDTO{
|
||||
ItemName: itemName,
|
||||
UOMName: itemUOM,
|
||||
}
|
||||
|
||||
if budget != nil {
|
||||
dto.BudgetQuantity = budget.Qty
|
||||
dto.BudgetUnitPrice = budget.Price
|
||||
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
|
||||
}
|
||||
|
||||
if realization != nil {
|
||||
dto.ActualQuantity = realization.Qty
|
||||
dto.ActualUnitPrice = realization.Price
|
||||
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
|
||||
dto.ActualDate = formatRealizationDate(realization)
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||
latestDateByNonstockID := make(map[uint]string)
|
||||
|
||||
for i := range budgets {
|
||||
nonstockID := budgets[i].NonstockId
|
||||
if overheadsByNonstockID[nonstockID] == nil {
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||
}
|
||||
|
||||
for i := range realizations {
|
||||
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
|
||||
if overheadsByNonstockID[nonstockID] == nil {
|
||||
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||
}
|
||||
|
||||
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||
|
||||
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||
}
|
||||
|
||||
realizationDateStr := formatRealizationDate(&realizations[i])
|
||||
if realizationDateStr != "" {
|
||||
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
|
||||
latestDateByNonstockID[nonstockID] = realizationDateStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
|
||||
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
|
||||
|
||||
for nonstockID, overhead := range overheadsByNonstockID {
|
||||
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||
|
||||
if overhead.ActualQuantity > 0 {
|
||||
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||
}
|
||||
|
||||
totalBudgetQuantity += overhead.BudgetQuantity
|
||||
totalBudgetAmount += overhead.BudgetTotalAmount
|
||||
totalActualQuantity += overhead.ActualQuantity
|
||||
totalActualAmount += overhead.ActualTotalAmount
|
||||
|
||||
overheadItems = append(overheadItems, *overhead)
|
||||
}
|
||||
|
||||
return OverheadListDTO{
|
||||
Total: TotalDTO{
|
||||
BudgetQuantity: totalBudgetQuantity,
|
||||
BudgetTotalAmount: totalBudgetAmount,
|
||||
ActualQuantity: totalActualQuantity,
|
||||
ActualTotalAmount: totalActualAmount,
|
||||
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||
},
|
||||
Overheads: overheadItems,
|
||||
}
|
||||
}
|
||||
|
||||
// === Helper Functions ===
|
||||
|
||||
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||
if nonstock != nil && nonstock.Id != 0 {
|
||||
return nonstock.Name, nonstock.Uom.Name
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func calculateTotal(qty, price float64) float64 {
|
||||
return qty * price
|
||||
}
|
||||
|
||||
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||
if totalChickinQty > 0 {
|
||||
return totalPrice / totalChickinQty
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func formatRealizationDate(realization *entity.ExpenseRealization) string {
|
||||
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
|
||||
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
|
||||
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SapronakDetailDTO struct {
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Flag string `json:"flag"`
|
||||
Tanggal *time.Time `json:"tanggal,omitempty"`
|
||||
NoReferensi string `json:"no_referensi,omitempty"`
|
||||
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
|
||||
QtyMasuk float64 `json:"qty_masuk"`
|
||||
QtyKeluar float64 `json:"qty_keluar"`
|
||||
Harga float64 `json:"harga"`
|
||||
Nilai float64 `json:"nilai"`
|
||||
}
|
||||
|
||||
type SapronakGroupDTO struct {
|
||||
Flag string `json:"flag"`
|
||||
Items []SapronakDetailDTO `json:"items"`
|
||||
TotalMasuk float64 `json:"total_masuk"`
|
||||
TotalKeluar float64 `json:"total_keluar"`
|
||||
SaldoAkhir float64 `json:"saldo_akhir"`
|
||||
TotalNilai float64 `json:"total_nilai"`
|
||||
}
|
||||
|
||||
type SapronakItemDTO struct {
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
Flag string `json:"flag"`
|
||||
IncomingQty float64 `json:"incoming_qty"`
|
||||
IncomingValue float64 `json:"incoming_value"`
|
||||
UsageQty float64 `json:"usage_qty"`
|
||||
UsageValue float64 `json:"usage_value"`
|
||||
RemainingQty float64 `json:"remaining_qty"`
|
||||
AveragePrice float64 `json:"average_price"`
|
||||
}
|
||||
|
||||
type SapronakReportDTO struct {
|
||||
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||
ProjectFlockID uint `json:"project_flock_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
KandangID uint `json:"kandang_id"`
|
||||
KandangName string `json:"kandang_name"`
|
||||
Period int `json:"period"`
|
||||
Status string `json:"status"`
|
||||
StartDate *time.Time `json:"start_date,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
TotalIncomingValue float64 `json:"total_incoming_value"`
|
||||
TotalUsageValue float64 `json:"total_usage_value"`
|
||||
Items []SapronakItemDTO `json:"items"`
|
||||
Groups []SapronakGroupDTO `json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// Simplified view for project-level sapronak response
|
||||
type SapronakCategoryRowDTO struct {
|
||||
ID int `json:"id"`
|
||||
Date string `json:"date"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
QtyIn float64 `json:"qty_in"`
|
||||
QtyOut float64 `json:"qty_out"`
|
||||
QtyUsed float64 `json:"qty_used"`
|
||||
Description string `json:"description"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
type SapronakCategoryTotalDTO struct {
|
||||
Label string `json:"label"`
|
||||
QtyIn float64 `json:"qty_in"`
|
||||
QtyOut float64 `json:"qty_out"`
|
||||
QtyUsed float64 `json:"qty_used"`
|
||||
AvgUnitPrice float64 `json:"avg_unit_price"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
}
|
||||
|
||||
type SapronakCategoryDTO struct {
|
||||
Rows []SapronakCategoryRowDTO `json:"rows"`
|
||||
Total SapronakCategoryTotalDTO `json:"total"`
|
||||
}
|
||||
|
||||
type SapronakProjectAggregatedDTO struct {
|
||||
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
|
||||
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
|
||||
}
|
||||
|
||||
type ClosingSapronakItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Date string `json:"date"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
ProductSubCategory string `json:"product_sub_category"`
|
||||
SourceWarehouse string `json:"source_warehouse"`
|
||||
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
||||
// Destination string `json:"destination,omitempty"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Unit string `json:"unit"`
|
||||
FormattedQuantity string `json:"formatted_quantity"`
|
||||
Notes string `json:"notes"`
|
||||
SortDate time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type ClosingSapronakDTO struct {
|
||||
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||
}
|
||||
|
||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if len(reports) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
rep := reports[0]
|
||||
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||
}
|
||||
|
||||
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if report == nil {
|
||||
report = &SapronakReportDTO{}
|
||||
}
|
||||
|
||||
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||
|
||||
byFlag := map[string]**SapronakCategoryDTO{}
|
||||
if filter == "" || filter == "DOC" {
|
||||
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["DOC"] = &result.Doc
|
||||
}
|
||||
if filter == "" || filter == "OVK" {
|
||||
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["OVK"] = &result.Ovk
|
||||
}
|
||||
if filter == "" || filter == "PAKAN" {
|
||||
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["PAKAN"] = &result.Pakan
|
||||
}
|
||||
if filter == "" || filter == "PULLET" {
|
||||
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
|
||||
byFlag["PULLET"] = &result.Pullet
|
||||
}
|
||||
|
||||
formatDate := func(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("02-Jan-2006")
|
||||
}
|
||||
|
||||
for _, group := range report.Groups {
|
||||
flagKey := strings.ToUpper(group.Flag)
|
||||
ptr := byFlag[flagKey]
|
||||
if ptr == nil || *ptr == nil {
|
||||
continue
|
||||
}
|
||||
target := *ptr
|
||||
|
||||
rowIndexByProduct := make(map[string]int)
|
||||
|
||||
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
|
||||
if idx, ok := rowIndexByProduct[productKey]; ok {
|
||||
return &target.Rows[idx]
|
||||
}
|
||||
target.Rows = append(target.Rows, base)
|
||||
idx := len(target.Rows) - 1
|
||||
rowIndexByProduct[productKey] = idx
|
||||
return &target.Rows[idx]
|
||||
}
|
||||
|
||||
for idx, item := range group.Items {
|
||||
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
|
||||
baseRow := SapronakCategoryRowDTO{
|
||||
ID: idx + 1,
|
||||
Date: formatDate(item.Tanggal),
|
||||
ReferenceNumber: item.NoReferensi,
|
||||
Description: item.ProductName,
|
||||
ProductCategory: item.ProductName,
|
||||
UnitPrice: item.Harga,
|
||||
Notes: "-",
|
||||
}
|
||||
|
||||
row := getOrCreateRow(productKey, baseRow)
|
||||
|
||||
switch strings.ToLower(item.JenisTransaksi) {
|
||||
case "pembelian", "adjustment masuk", "mutasi masuk":
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
case "pemakaian", "adjustment keluar":
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
case "mutasi keluar":
|
||||
row.QtyOut += item.QtyKeluar
|
||||
default:
|
||||
row.QtyIn += item.QtyMasuk
|
||||
row.TotalAmount += item.Nilai
|
||||
}
|
||||
|
||||
if row.QtyIn > 0 {
|
||||
row.UnitPrice = row.TotalAmount / row.QtyIn
|
||||
}
|
||||
}
|
||||
|
||||
for i := range target.Rows {
|
||||
target.Rows[i].ID = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
buildTotals := func(cat *SapronakCategoryDTO, label string) {
|
||||
if cat == nil {
|
||||
return
|
||||
}
|
||||
var qtyIn, qtyOut, qtyUsed, total float64
|
||||
for _, r := range cat.Rows {
|
||||
qtyIn += r.QtyIn
|
||||
qtyOut += r.QtyOut
|
||||
qtyUsed += r.QtyUsed
|
||||
total += r.TotalAmount
|
||||
}
|
||||
avg := 0.0
|
||||
if qtyIn > 0 {
|
||||
avg = total / qtyIn
|
||||
}
|
||||
cat.Total = SapronakCategoryTotalDTO{
|
||||
Label: label,
|
||||
QtyIn: qtyIn,
|
||||
QtyOut: qtyOut,
|
||||
QtyUsed: qtyUsed,
|
||||
AvgUnitPrice: avg,
|
||||
TotalAmount: total,
|
||||
}
|
||||
}
|
||||
|
||||
buildTotals(result.Doc, "TOTAL DOC")
|
||||
buildTotals(result.Ovk, "TOTAL OVK")
|
||||
buildTotals(result.Pakan, "TOTAL PAKAN")
|
||||
buildTotals(result.Pullet, "TOTAL PULLET")
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
@@ -22,13 +24,18 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
closingRepo := rClosing.NewClosingRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||
chickinRepo := rChickin.NewChickinRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ClosingRoutes(router, userService, closingService)
|
||||
ClosingRoutes(router, userService, closingService, sapronakService)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"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]
|
||||
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
|
||||
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
}
|
||||
|
||||
type ClosingRepositoryImpl struct {
|
||||
@@ -19,3 +35,566 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
|
||||
}
|
||||
}
|
||||
|
||||
type SapronakRow struct {
|
||||
Id uint64 `gorm:"column:id"`
|
||||
SortDate time.Time `gorm:"column:sort_date"`
|
||||
DateText string `gorm:"column:date_text"`
|
||||
ReferenceNumber string `gorm:"column:reference_number"`
|
||||
TransactionType string `gorm:"column:transaction_type"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
ProductCategory string `gorm:"column:product_category"`
|
||||
ProductSubCategory string `gorm:"column:product_sub_category"`
|
||||
SourceWarehouse string `gorm:"column:source_warehouse"`
|
||||
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||
Destination string `gorm:"column:destination"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
Unit string `gorm:"column:unit"`
|
||||
Notes string `gorm:"column:notes"`
|
||||
}
|
||||
|
||||
type SapronakQueryParams struct {
|
||||
Type string
|
||||
WarehouseIDs []uint
|
||||
ProjectFlockKandangIDs []uint
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
var (
|
||||
unionParts []string
|
||||
args []any
|
||||
)
|
||||
|
||||
switch params.Type {
|
||||
case validation.SapronakTypeIncoming:
|
||||
if len(params.WarehouseIDs) == 0 {
|
||||
return []SapronakRow{}, 0, nil
|
||||
}
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
case validation.SapronakTypeOutgoing:
|
||||
if len(params.WarehouseIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs)
|
||||
}
|
||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||
args = append(args, params.ProjectFlockKandangIDs)
|
||||
}
|
||||
if len(unionParts) == 0 {
|
||||
return []SapronakRow{}, 0, nil
|
||||
}
|
||||
default:
|
||||
return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||
}
|
||||
|
||||
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||
|
||||
var totalResults int64
|
||||
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||
|
||||
var rows []SapronakRow
|
||||
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, totalResults, nil
|
||||
}
|
||||
|
||||
const (
|
||||
sapronakIncomingPurchasesSQL = `
|
||||
SELECT
|
||||
CAST(pi.id AS BIGINT) AS id,
|
||||
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||
COALESCE(p.po_number, '') AS reference_number,
|
||||
'Purchase' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
'External Supplier' AS source_warehouse,
|
||||
w.name AS destination_warehouse,
|
||||
'' AS destination,
|
||||
pi.total_qty AS quantity,
|
||||
u.name AS unit,
|
||||
COALESCE(p.notes, '') AS notes
|
||||
FROM purchase_items pi
|
||||
JOIN purchases p ON p.id = pi.purchase_id
|
||||
JOIN products prod ON prod.id = pi.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||
WHERE pi.warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakIncomingTransfersSQL = `
|
||||
SELECT
|
||||
CAST(st.id AS BIGINT) AS id,
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer In' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
COALESCE(tw.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
std.quantity AS quantity,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.to_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakOutgoingTransfersSQL = `
|
||||
SELECT
|
||||
CAST(st.id AS BIGINT) AS id,
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer Out' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(fw.name, '') AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
COALESCE(tw.name, '') AS destination,
|
||||
std.quantity AS quantity,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||
JOIN products prod ON prod.id = std.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.from_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakOutgoingMarketingsSQL = `
|
||||
SELECT
|
||||
CAST(mp.id AS BIGINT) AS id,
|
||||
m.so_date AS sort_date,
|
||||
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||
m.so_number AS reference_number,
|
||||
'Trading Sales' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
pc.name AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(f.name, ' ')
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
w.name AS source_warehouse,
|
||||
'' AS destination_warehouse,
|
||||
'RETAIL CUSTOMER' AS destination,
|
||||
mp.qty AS quantity,
|
||||
u.name AS unit,
|
||||
m.notes AS notes
|
||||
FROM marketing_products mp
|
||||
JOIN marketings m ON m.id = mp.marketing_id
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
WHERE pw.project_flock_kandang_id IN ?
|
||||
`
|
||||
)
|
||||
|
||||
type SapronakIncomingRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
Value float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
type SapronakUsageRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Qty float64
|
||||
DefaultPrice float64
|
||||
}
|
||||
|
||||
type SapronakDetailRow struct {
|
||||
ProductID uint
|
||||
ProductName string
|
||||
Flag string
|
||||
Date *time.Time
|
||||
Reference string
|
||||
QtyIn float64
|
||||
QtyOut float64
|
||||
Price float64
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
|
||||
|
||||
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
|
||||
for _, j := range joins {
|
||||
if strings.TrimSpace(j) != "" {
|
||||
db = db.Joins(j)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func sapronakFlags(flags ...utils.FlagType) []string {
|
||||
out := make([]string, len(flags))
|
||||
for i, f := range flags {
|
||||
out[i] = string(f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var (
|
||||
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
|
||||
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
|
||||
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
|
||||
)
|
||||
|
||||
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
|
||||
m := make(map[uint][]SapronakDetailRow)
|
||||
for _, row := range rows {
|
||||
m[row.ProductID] = append(m[row.ProductID], row)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
|
||||
rows := make([]SapronakDetailRow, 0)
|
||||
if err := db.Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return groupSapronakDetails(rows), nil
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Usage (summary + details)
|
||||
// =========================
|
||||
|
||||
func (r *ClosingRepositoryImpl) usageQuery(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
where string,
|
||||
args ...any,
|
||||
) *gorm.DB {
|
||||
db := r.withCtx(ctx).Table(table).Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(usage_qty), 0) AS qty,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`)
|
||||
db = applyJoins(db, joins...)
|
||||
return db.
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Where(where, args...)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
where string,
|
||||
args ...any,
|
||||
) ([]SapronakUsageRow, error) {
|
||||
rows := make([]SapronakUsageRow, 0)
|
||||
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
|
||||
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) detailQuery(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
selectSQL string,
|
||||
where string,
|
||||
args ...any,
|
||||
) *gorm.DB {
|
||||
db := r.withCtx(ctx).
|
||||
Table(table).
|
||||
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
|
||||
|
||||
db = applyJoins(db, joins...)
|
||||
return db.Select(selectSQL).Where(where, args...)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
|
||||
ctx context.Context,
|
||||
table string,
|
||||
pwJoinCond string,
|
||||
joins []string,
|
||||
selectSQL string,
|
||||
where string,
|
||||
args ...any,
|
||||
) (map[uint][]SapronakDetailRow, error) {
|
||||
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||
if pfkID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return r.fetchSapronakUsage(
|
||||
ctx,
|
||||
"recording_stocks rs",
|
||||
"pw.id = rs.product_warehouse_id",
|
||||
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
|
||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsUsage,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
|
||||
if pfkID == 0 {
|
||||
return []SapronakUsageRow{}, nil
|
||||
}
|
||||
return r.fetchSapronakUsage(
|
||||
ctx,
|
||||
"project_chickins pc",
|
||||
"pw.id = pc.product_warehouse_id",
|
||||
nil,
|
||||
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsChickin,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return r.fetchSapronakDetails(
|
||||
ctx,
|
||||
"recording_stocks rs",
|
||||
"pw.id = rs.product_warehouse_id",
|
||||
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
|
||||
`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
r.record_datetime AS date,
|
||||
CAST(r.id AS TEXT) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(rs.usage_qty,0) AS qty_out,
|
||||
COALESCE(p.product_price,0) AS price
|
||||
`,
|
||||
"r.project_flock_kandangs_id = ? AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsUsage,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return r.fetchSapronakDetails(
|
||||
ctx,
|
||||
"project_chickins pc",
|
||||
"pw.id = pc.product_warehouse_id",
|
||||
nil,
|
||||
`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
pc.chick_in_date AS date,
|
||||
CAST(pc.id AS TEXT) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(pc.usage_qty,0) AS qty_out,
|
||||
COALESCE(p.product_price,0) AS price
|
||||
`,
|
||||
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
|
||||
pfkID,
|
||||
sapronakFlagsChickin,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
|
||||
return r.withCtx(ctx).
|
||||
Table("purchase_items AS pi").
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
|
||||
Joins("JOIN products p ON p.id = pi.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where("pi.received_date IS NOT NULL")
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
|
||||
rows := make([]SapronakIncomingRow, 0)
|
||||
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||
pi.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(SUM(pi.total_qty), 0) AS qty,
|
||||
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
|
||||
COALESCE(p.product_price, 0) AS default_price
|
||||
`)
|
||||
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
return scanAndGroupDetails(
|
||||
r.incomingPurchaseBase(ctx, kandangID).Select(`
|
||||
pi.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
pi.received_date AS date,
|
||||
COALESCE(po.po_number, '') AS reference,
|
||||
COALESCE(pi.total_qty,0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(pi.price,0) AS price
|
||||
`),
|
||||
)
|
||||
}
|
||||
|
||||
type stockLogSapronakRow struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
ProductID uint `gorm:"column:product_id"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
Flag string `gorm:"column:flag"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
Increase float64 `gorm:"column:increase"`
|
||||
Decrease float64 `gorm:"column:decrease"`
|
||||
Price float64 `gorm:"column:price"`
|
||||
MovementNumber string `gorm:"column:movement_number"`
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
|
||||
rows := make([]stockLogSapronakRow, 0)
|
||||
|
||||
movementSelect := "'' AS movement_number"
|
||||
joins := []string{}
|
||||
if withMovement {
|
||||
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
|
||||
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
|
||||
}
|
||||
|
||||
db := r.withCtx(ctx).
|
||||
Table("stock_logs sl").
|
||||
Select(`
|
||||
sl.id AS id,
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
sl.created_at AS created_at,
|
||||
COALESCE(sl.increase,0) AS increase,
|
||||
COALESCE(sl.decrease,0) AS decrease,
|
||||
COALESCE(p.product_price,0) AS price,
|
||||
` + movementSelect + `
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
|
||||
|
||||
db = applyJoins(db, joins...)
|
||||
|
||||
if err := db.
|
||||
Where("sl.loggable_type = ?", logType).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
|
||||
in := make(map[uint][]SapronakDetailRow)
|
||||
out := make(map[uint][]SapronakDetailRow)
|
||||
|
||||
for _, row := range rows {
|
||||
base := SapronakDetailRow{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
Date: row.CreatedAt,
|
||||
Reference: refFn(row),
|
||||
Price: row.Price,
|
||||
}
|
||||
|
||||
if row.Increase > 0 {
|
||||
d := base
|
||||
d.QtyIn = row.Increase
|
||||
in[row.ProductID] = append(in[row.ProductID], d)
|
||||
}
|
||||
if row.Decrease > 0 {
|
||||
d := base
|
||||
d.QtyOut = row.Decrease
|
||||
out[row.ProductID] = append(out[row.ProductID], d)
|
||||
}
|
||||
}
|
||||
|
||||
return in, out
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||
return in, out, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
|
||||
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
|
||||
return ref
|
||||
}
|
||||
return fmt.Sprintf("TRF-%d", row.ID)
|
||||
})
|
||||
return in, out, nil
|
||||
}
|
||||
@@ -9,11 +9,12 @@ 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) {
|
||||
ctrl := controller.NewClosingController(s, sapronakSvc)
|
||||
|
||||
route := v1.Group("/closing")
|
||||
route := v1.Group("/closings")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
@@ -23,4 +24,11 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
||||
route.Get("/",m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan",m.RequirePermissions(m.P_ClosingPenjualan), ctrl.GetPenjualan)
|
||||
route.Get("/:projectFlockId",m.RequirePermissions(m.P_ClosingGetSummary), ctrl.GetClosingSummary)
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
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"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
@@ -22,10 +25,12 @@ import (
|
||||
)
|
||||
|
||||
type ClosingService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||
}
|
||||
|
||||
type closingService struct {
|
||||
@@ -36,9 +41,12 @@ type closingService struct {
|
||||
MarketingRepo marketingRepository.MarketingRepository
|
||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||
}
|
||||
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
|
||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
|
||||
return &closingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -47,6 +55,9 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||
ProjectBudgetRepo: projectBudgetRepo,
|
||||
ChickinRepo: chickinRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +67,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
|
||||
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db).
|
||||
Preload("Location").
|
||||
Preload("KandangHistory").
|
||||
Preload("KandangHistory.Chickins")
|
||||
}
|
||||
|
||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -68,9 +80,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
db = s.withClosingRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -79,7 +91,19 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
||||
s.Log.Errorf("Failed to get closings: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return closings, total, nil
|
||||
|
||||
result := make([]dto.ClosingListItemDTO, 0, len(closings))
|
||||
for _, closing := range closings {
|
||||
statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||
}
|
||||
|
||||
result = append(result, dto.ToClosingListItemDTO(closing, statusProject))
|
||||
}
|
||||
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||
@@ -144,6 +168,147 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||
}
|
||||
|
||||
if params == nil {
|
||||
params = &validation.ClosingSapronakQuery{}
|
||||
}
|
||||
|
||||
if params.Page == 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Limit == 0 {
|
||||
params.Limit = 10
|
||||
}
|
||||
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||
}
|
||||
|
||||
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||
}
|
||||
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||
}
|
||||
|
||||
var projectFlockKandangIDs []uint
|
||||
if params.Type == validation.SapronakTypeOutgoing {
|
||||
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{
|
||||
Type: params.Type,
|
||||
WarehouseIDs: warehouseIDs,
|
||||
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||
Limit: params.Limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data")
|
||||
}
|
||||
|
||||
items := make([]dto.ClosingSapronakItemDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
dateStr := row.DateText
|
||||
if dateStr == "" && !row.SortDate.IsZero() {
|
||||
dateStr = row.SortDate.Format("02-Jan-2006")
|
||||
}
|
||||
items = append(items, dto.ClosingSapronakItemDTO{
|
||||
Id: row.Id,
|
||||
Date: dateStr,
|
||||
ReferenceNumber: row.ReferenceNumber,
|
||||
TransactionType: row.TransactionType,
|
||||
ProductName: row.ProductName,
|
||||
ProductCategory: row.ProductCategory,
|
||||
ProductSubCategory: row.ProductSubCategory,
|
||||
SourceWarehouse: row.SourceWarehouse,
|
||||
DestinationWarehouse: row.DestinationWarehouse,
|
||||
// Destination: row.Destination,
|
||||
Quantity: row.Quantity,
|
||||
Unit: row.Unit,
|
||||
FormattedQuantity: formatQuantity(row.Quantity, row.Unit),
|
||||
Notes: row.Notes,
|
||||
SortDate: row.SortDate,
|
||||
})
|
||||
}
|
||||
|
||||
return items, totalResults, nil
|
||||
}
|
||||
|
||||
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
var kandangIDs []uint
|
||||
db := s.Repository.DB().WithContext(ctx)
|
||||
|
||||
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("kandang_id", &kandangIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(kandangIDs) == 0 {
|
||||
return []uint{}, nil
|
||||
}
|
||||
|
||||
var warehouses []entity.Warehouse
|
||||
if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unique := make(map[uint]struct{})
|
||||
for _, warehouse := range warehouses {
|
||||
unique[warehouse.Id] = struct{}{}
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(unique))
|
||||
for id := range unique {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := s.Repository.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func formatQuantity(qty float64, uom string) string {
|
||||
qtyStr := strconv.FormatFloat(qty, 'f', -1, 64)
|
||||
if uom == "" {
|
||||
return qtyStr
|
||||
}
|
||||
return qtyStr + " " + uom
|
||||
}
|
||||
|
||||
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||
if s.ApprovalSvc == nil {
|
||||
return "", "Belum Selesai", nil
|
||||
@@ -165,7 +330,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++
|
||||
}
|
||||
}
|
||||
@@ -188,3 +353,29 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
|
||||
return statusProject, statusClosing, nil
|
||||
}
|
||||
|
||||
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalChickinQty float64
|
||||
for _, chickin := range chickins {
|
||||
totalChickinQty += chickin.UsageQty
|
||||
}
|
||||
|
||||
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,681 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type SapronakService interface {
|
||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||
}
|
||||
|
||||
type sapronakService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ClosingRepository
|
||||
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewSapronakService(
|
||||
repo repository.ClosingRepository,
|
||||
pfkRepo projectflockRepository.ProjectFlockKandangRepository,
|
||||
validate *validator.Validate,
|
||||
) SapronakService {
|
||||
return &sapronakService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockKandangRepo: pfkRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||
}
|
||||
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
Status: "all",
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reports) <= 1 {
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||
return []dto.SapronakReportDTO{combined}, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||
if projectFlockID == 0 || pfkID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||
}
|
||||
|
||||
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
ProjectFlockKandangID: pfkID,
|
||||
Status: "all",
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||
return &res, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||
}
|
||||
|
||||
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pfks) == 0 {
|
||||
return []dto.SapronakReportDTO{}, nil
|
||||
}
|
||||
|
||||
filterStatus := strings.ToLower(strings.TrimSpace(params.Status))
|
||||
if filterStatus == "" {
|
||||
filterStatus = "all"
|
||||
}
|
||||
|
||||
results := make([]dto.SapronakReportDTO, 0, len(pfks))
|
||||
for _, pfk := range pfks {
|
||||
status := "closing"
|
||||
if pfk.ClosedAt == nil {
|
||||
status = "active"
|
||||
}
|
||||
|
||||
if (filterStatus == "active" && status != "active") || (filterStatus == "closing" && status != "closing") {
|
||||
continue
|
||||
}
|
||||
|
||||
// We no longer filter by date for closing sapronak report; pass nil pointers.
|
||||
items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, nil, nil, params.Flag)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report")
|
||||
}
|
||||
|
||||
results = append(results, dto.SapronakReportDTO{
|
||||
ProjectFlockKandangID: pfk.Id,
|
||||
ProjectFlockID: pfk.ProjectFlockId,
|
||||
ProjectName: pfk.ProjectFlock.FlockName,
|
||||
KandangID: pfk.KandangId,
|
||||
KandangName: pfk.Kandang.Name,
|
||||
Period: pfk.Period,
|
||||
Status: status,
|
||||
StartDate: nil,
|
||||
EndDate: nil,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("Kandang").
|
||||
Preload("Chickins")
|
||||
|
||||
if params != nil {
|
||||
if params.ProjectFlockID > 0 {
|
||||
db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockID)
|
||||
}
|
||||
if params.KandangID > 0 {
|
||||
db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangID)
|
||||
}
|
||||
if params.ProjectFlockKandangID > 0 {
|
||||
db = db.Where("project_flock_kandangs.id = ?", params.ProjectFlockKandangID)
|
||||
}
|
||||
}
|
||||
|
||||
var pfks []entity.ProjectFlockKandang
|
||||
if err := db.Find(&pfks).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandangs")
|
||||
}
|
||||
return pfks, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) combineSapronakReports(reports []dto.SapronakReportDTO, projectID uint) dto.SapronakReportDTO {
|
||||
if len(reports) == 0 {
|
||||
return dto.SapronakReportDTO{}
|
||||
}
|
||||
|
||||
var (
|
||||
totalIncoming float64
|
||||
totalUsage float64
|
||||
projectName = reports[0].ProjectName
|
||||
)
|
||||
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO)
|
||||
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
return g
|
||||
}
|
||||
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||
return groupMap[flag]
|
||||
}
|
||||
|
||||
for _, r := range reports {
|
||||
totalIncoming += r.TotalIncomingValue
|
||||
totalUsage += r.TotalUsageValue
|
||||
|
||||
for _, it := range r.Items {
|
||||
cur := itemMap[it.ProductID]
|
||||
if cur.ProductID == 0 {
|
||||
cur.ProductID = it.ProductID
|
||||
cur.ProductName = it.ProductName
|
||||
cur.Flag = it.Flag
|
||||
}
|
||||
cur.IncomingQty += it.IncomingQty
|
||||
cur.IncomingValue += it.IncomingValue
|
||||
cur.UsageQty += it.UsageQty
|
||||
cur.UsageValue += it.UsageValue
|
||||
if cur.IncomingQty >= cur.UsageQty {
|
||||
cur.RemainingQty = cur.IncomingQty - cur.UsageQty
|
||||
} else {
|
||||
cur.RemainingQty = 0
|
||||
}
|
||||
if cur.IncomingQty > 0 {
|
||||
cur.AveragePrice = cur.IncomingValue / cur.IncomingQty
|
||||
} else {
|
||||
cur.AveragePrice = it.AveragePrice
|
||||
}
|
||||
itemMap[it.ProductID] = cur
|
||||
}
|
||||
|
||||
for _, g := range r.Groups {
|
||||
agg := ensureGroup(g.Flag)
|
||||
agg.TotalMasuk += g.TotalMasuk
|
||||
agg.TotalKeluar += g.TotalKeluar
|
||||
agg.SaldoAkhir += g.SaldoAkhir
|
||||
agg.TotalNilai += g.TotalNilai
|
||||
agg.Items = append(agg.Items, g.Items...)
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||
for _, it := range itemMap {
|
||||
items = append(items, it)
|
||||
}
|
||||
|
||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
}
|
||||
|
||||
return dto.SapronakReportDTO{
|
||||
ProjectFlockID: projectID,
|
||||
ProjectName: projectName,
|
||||
Status: "combined",
|
||||
StartDate: nil,
|
||||
TotalIncomingValue: totalIncoming,
|
||||
TotalUsageValue: totalUsage,
|
||||
Items: items,
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) {
|
||||
incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows))
|
||||
for _, row := range incomingRows {
|
||||
incoming[row.ProductID] = row
|
||||
}
|
||||
usage := make(map[uint]repository.SapronakUsageRow, len(usageRows))
|
||||
for _, row := range usageRows {
|
||||
usage[row.ProductID] = row
|
||||
}
|
||||
return incoming, usage
|
||||
}
|
||||
|
||||
type sapronakDetailMaps struct {
|
||||
Incoming map[uint][]dto.SapronakDetailDTO
|
||||
Usage map[uint][]dto.SapronakDetailDTO
|
||||
AdjIncoming map[uint][]dto.SapronakDetailDTO
|
||||
AdjOutgoing map[uint][]dto.SapronakDetailDTO
|
||||
TransferIn map[uint][]dto.SapronakDetailDTO
|
||||
TransferOut map[uint][]dto.SapronakDetailDTO
|
||||
}
|
||||
|
||||
func buildSapronakDetails(
|
||||
incomingRows map[uint][]repository.SapronakDetailRow,
|
||||
usageRows map[uint][]repository.SapronakDetailRow,
|
||||
adjIncomingRows map[uint][]repository.SapronakDetailRow,
|
||||
adjOutgoingRows map[uint][]repository.SapronakDetailRow,
|
||||
transferInRows map[uint][]repository.SapronakDetailRow,
|
||||
transferOutRows map[uint][]repository.SapronakDetailRow,
|
||||
) sapronakDetailMaps {
|
||||
result := sapronakDetailMaps{
|
||||
Incoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
Usage: make(map[uint][]dto.SapronakDetailDTO),
|
||||
AdjIncoming: make(map[uint][]dto.SapronakDetailDTO),
|
||||
AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferIn: make(map[uint][]dto.SapronakDetailDTO),
|
||||
TransferOut: make(map[uint][]dto.SapronakDetailDTO),
|
||||
}
|
||||
|
||||
addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) {
|
||||
for pid, rows := range src {
|
||||
for _, r := range rows {
|
||||
d := dto.SapronakDetailDTO{
|
||||
ProductID: r.ProductID,
|
||||
ProductName: r.ProductName,
|
||||
Flag: r.Flag,
|
||||
Tanggal: r.Date,
|
||||
NoReferensi: r.Reference,
|
||||
JenisTransaksi: jenis,
|
||||
Harga: r.Price,
|
||||
}
|
||||
if masuk {
|
||||
d.QtyMasuk = r.QtyIn
|
||||
d.Nilai = r.QtyIn * r.Price
|
||||
} else {
|
||||
d.QtyKeluar = r.QtyOut
|
||||
d.Nilai = r.QtyOut * r.Price
|
||||
}
|
||||
target[pid] = append(target[pid], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) {
|
||||
// For sapronak closing report we intentionally ignore date range
|
||||
// and aggregate all historical transactions for the kandang/project.
|
||||
incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
incomingDetailsRows, err := s.Repository.FetchSapronakIncomingDetails(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageRows, err := s.Repository.FetchSapronakUsage(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
chickinUsageRows, err := s.Repository.FetchSapronakChickinUsage(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
usageDetailsRows, err := s.Repository.FetchSapronakUsageDetails(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
chickinUsageDetailsRows, err := s.Repository.FetchSapronakChickinUsageDetails(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
adjIncomingRows, adjOutgoingRows, err := s.Repository.FetchSapronakAdjustments(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
transIncomingRows, transOutgoingRows, err := s.Repository.FetchSapronakTransfers(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
|
||||
filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter))
|
||||
matchesFlag := func(f string) bool {
|
||||
if filterFlag == "" {
|
||||
return true
|
||||
}
|
||||
return strings.ToUpper(f) == filterFlag
|
||||
}
|
||||
|
||||
// For project flocks with category GROWING, pullet usage from chickin
|
||||
// should not be counted yet. Only when category is LAYING we allow
|
||||
// pullet usage to contribute to qty_used.
|
||||
isLaying := strings.EqualFold(string(pfk.ProjectFlock.Category), string(utils.ProjectFlockCategoryLaying))
|
||||
|
||||
if !isLaying {
|
||||
filteredUsage := make([]repository.SapronakUsageRow, 0, len(chickinUsageRows))
|
||||
for _, row := range chickinUsageRows {
|
||||
if strings.ToUpper(row.Flag) == "DOC" {
|
||||
filteredUsage = append(filteredUsage, row)
|
||||
}
|
||||
}
|
||||
chickinUsageRows = filteredUsage
|
||||
|
||||
filteredDetail := make(map[uint][]repository.SapronakDetailRow, len(chickinUsageDetailsRows))
|
||||
for pid, rows := range chickinUsageDetailsRows {
|
||||
for _, d := range rows {
|
||||
if strings.ToUpper(d.Flag) == "DOC" {
|
||||
filteredDetail[pid] = append(filteredDetail[pid], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
chickinUsageDetailsRows = filteredDetail
|
||||
}
|
||||
|
||||
allUsageRows := append(usageRows, chickinUsageRows...)
|
||||
incoming, usage := mapIncomingUsage(incomingRows, allUsageRows)
|
||||
itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage))
|
||||
groupMap := make(map[string]*dto.SapronakGroupDTO)
|
||||
|
||||
for pid, rows := range chickinUsageDetailsRows {
|
||||
if len(rows) == 0 {
|
||||
continue
|
||||
}
|
||||
usageDetailsRows[pid] = append(usageDetailsRows[pid], rows...)
|
||||
}
|
||||
|
||||
detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows, transOutgoingRows)
|
||||
incomingDetails := detailMaps.Incoming
|
||||
usageDetails := detailMaps.Usage
|
||||
adjIncoming := detailMaps.AdjIncoming
|
||||
adjOutgoing := detailMaps.AdjOutgoing
|
||||
transIncoming := detailMaps.TransferIn
|
||||
transOutgoing := detailMaps.TransferOut
|
||||
|
||||
ensureGroup := func(flag string) *dto.SapronakGroupDTO {
|
||||
if g, ok := groupMap[flag]; ok {
|
||||
return g
|
||||
}
|
||||
groupMap[flag] = &dto.SapronakGroupDTO{Flag: flag}
|
||||
return groupMap[flag]
|
||||
}
|
||||
|
||||
for _, row := range incoming {
|
||||
if !matchesFlag(row.Flag) {
|
||||
continue
|
||||
}
|
||||
avgPrice := row.DefaultPrice
|
||||
if row.Qty > 0 && row.Value > 0 {
|
||||
avgPrice = row.Value / row.Qty
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = dto.SapronakItemDTO{
|
||||
ProductID: row.ProductID,
|
||||
ProductName: row.ProductName,
|
||||
Flag: row.Flag,
|
||||
IncomingQty: row.Qty,
|
||||
IncomingValue: row.Value,
|
||||
RemainingQty: row.Qty,
|
||||
AveragePrice: avgPrice,
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range usage {
|
||||
if !matchesFlag(row.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[row.ProductID]
|
||||
price := existing.AveragePrice
|
||||
if price == 0 {
|
||||
price = row.DefaultPrice
|
||||
}
|
||||
|
||||
usageValue := row.Qty * price
|
||||
|
||||
existing.ProductID = row.ProductID
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = row.ProductName
|
||||
}
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = row.Flag
|
||||
}
|
||||
existing.AveragePrice = price
|
||||
existing.UsageQty += row.Qty
|
||||
existing.UsageValue += usageValue
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
|
||||
itemMap[row.ProductID] = existing
|
||||
}
|
||||
|
||||
for productID, details := range adjIncoming {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.IncomingQty += d.QtyMasuk
|
||||
existing.IncomingValue += d.Nilai
|
||||
if existing.IncomingQty > 0 {
|
||||
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||
}
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjOutgoing {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.UsageQty += d.QtyKeluar
|
||||
existing.UsageValue += d.Nilai
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transIncoming {
|
||||
for _, d := range details {
|
||||
if !matchesFlag(d.Flag) {
|
||||
continue
|
||||
}
|
||||
existing := itemMap[productID]
|
||||
if existing.Flag == "" {
|
||||
existing.Flag = d.Flag
|
||||
}
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.IncomingQty += d.QtyMasuk
|
||||
existing.IncomingValue += d.Nilai
|
||||
if existing.IncomingQty > 0 {
|
||||
existing.AveragePrice = existing.IncomingValue / existing.IncomingQty
|
||||
}
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
}
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.SapronakItemDTO, 0, len(itemMap))
|
||||
var totalIncoming, totalUsage float64
|
||||
for _, item := range itemMap {
|
||||
totalIncoming += item.IncomingValue
|
||||
totalUsage += item.UsageValue
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
for productID, details := range incomingDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range usageDetails {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range adjOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transIncoming {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalMasuk += d.QtyMasuk
|
||||
group.TotalNilai += d.Nilai
|
||||
group.SaldoAkhir += d.QtyMasuk
|
||||
}
|
||||
}
|
||||
|
||||
for productID, details := range transOutgoing {
|
||||
flag := ""
|
||||
name := ""
|
||||
if item, ok := itemMap[productID]; ok {
|
||||
flag = item.Flag
|
||||
name = item.ProductName
|
||||
}
|
||||
if !matchesFlag(flag) {
|
||||
continue
|
||||
}
|
||||
group := ensureGroup(flag)
|
||||
for _, d := range details {
|
||||
d.Flag = flag
|
||||
d.ProductName = name
|
||||
group.Items = append(group.Items, d)
|
||||
group.TotalKeluar += d.QtyKeluar
|
||||
group.SaldoAkhir -= d.QtyKeluar
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]dto.SapronakGroupDTO, 0, len(groupMap))
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, *g)
|
||||
}
|
||||
|
||||
return items, groups, totalIncoming, totalUsage, nil
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -13,3 +13,14 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
const (
|
||||
SapronakTypeIncoming = "incoming"
|
||||
SapronakTypeOutgoing = "outgoing"
|
||||
)
|
||||
|
||||
type ClosingSapronakQuery struct {
|
||||
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package validation
|
||||
|
||||
type CountSapronakQuery struct {
|
||||
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=active closing all"`
|
||||
Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN PULLET doc ovk pakan pullet"`
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -13,6 +15,8 @@ type ExpenseRepository interface {
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetNextSequence(ctx context.Context) (int, error)
|
||||
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
||||
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
|
||||
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
|
||||
}
|
||||
|
||||
type ExpenseRepositoryImpl struct {
|
||||
@@ -49,3 +53,57 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
|
||||
}
|
||||
return &expense, nil
|
||||
}
|
||||
|
||||
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if pfkID == 0 && kandangID == 0 {
|
||||
return db
|
||||
}
|
||||
q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
|
||||
if pfkID > 0 && kandangID > 0 {
|
||||
return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
|
||||
}
|
||||
if pfkID > 0 {
|
||||
return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
|
||||
}
|
||||
return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
|
||||
if pfkID == 0 && kandangID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var ids []uint64
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Table("expenses").
|
||||
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
|
||||
Group("expenses.id").Where("expenses.deleted_at IS NULL").
|
||||
Pluck("expenses.id", &ids).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var unfinished int64
|
||||
for _, id := range ids {
|
||||
var latest entity.Approval
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("approvals").
|
||||
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
|
||||
Order("action_at DESC").
|
||||
Limit(1).
|
||||
First(&latest).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, err
|
||||
}
|
||||
if isFinished != nil {
|
||||
if !isFinished(&latest) {
|
||||
unfinished++
|
||||
}
|
||||
}
|
||||
}
|
||||
return unfinished, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"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/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,8 @@ type ExpenseRealizationRepository interface {
|
||||
repository.BaseRepository[entity.ExpenseRealization]
|
||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error)
|
||||
}
|
||||
|
||||
type ExpenseRealizationRepositoryImpl struct {
|
||||
@@ -30,11 +34,102 @@ func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint
|
||||
|
||||
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
||||
var realization entity.ExpenseRealization
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("expense_nonstock_id = ?", expenseNonstockID).
|
||||
First(&realization).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &realization, nil
|
||||
err := r.DB().WithContext(ctx).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
|
||||
return &realization, err
|
||||
}
|
||||
|
||||
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
|
||||
var realizations []entity.ExpenseRealization
|
||||
err := r.DB().WithContext(ctx).
|
||||
Preload("ExpenseNonstock").
|
||||
Preload("ExpenseNonstock.Nonstock").
|
||||
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||
Preload("ExpenseNonstock.Expense").
|
||||
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Where("expenses.category = ?", "BOP").
|
||||
Find(&realizations).Error
|
||||
return realizations, err
|
||||
}
|
||||
|
||||
func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) {
|
||||
var realizations []entity.ExpenseRealization
|
||||
var total int64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Model(&entity.ExpenseRealization{}).
|
||||
Preload("ExpenseNonstock", func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Expense").
|
||||
Preload("Expense.Supplier").
|
||||
Preload("Kandang").
|
||||
Preload("Kandang.Location").
|
||||
Preload("Nonstock")
|
||||
}).
|
||||
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||
Joins("LEFT JOIN suppliers ON suppliers.id = expenses.supplier_id")
|
||||
|
||||
if filters.Search != "" {
|
||||
db = db.Where("expenses.category LIKE ? OR expenses.reference_number LIKE ? OR expenses.po_number LIKE ? OR expenses.notes LIKE ? OR suppliers.name LIKE ?",
|
||||
"%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%", "%"+filters.Search+"%")
|
||||
}
|
||||
|
||||
if filters.Category != "" {
|
||||
db = db.Where("expenses.category = ?", filters.Category)
|
||||
}
|
||||
|
||||
if filters.SupplierId > 0 {
|
||||
db = db.Where("expenses.supplier_id = ?", filters.SupplierId)
|
||||
}
|
||||
|
||||
if filters.KandangId > 0 {
|
||||
db = db.Where("expense_nonstocks.kandang_id = ?", filters.KandangId)
|
||||
}
|
||||
|
||||
if filters.ProjectFlockKandangId > 0 {
|
||||
db = db.Where("expense_nonstocks.project_flock_kandang_id = ?", filters.ProjectFlockKandangId)
|
||||
}
|
||||
|
||||
if filters.NonstockId > 0 {
|
||||
db = db.Where("expense_nonstocks.nonstock_id = ?", filters.NonstockId)
|
||||
}
|
||||
|
||||
locationID := filters.LocationId
|
||||
areaID := filters.AreaId
|
||||
|
||||
if locationID > 0 || areaID > 0 {
|
||||
db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id")
|
||||
|
||||
if locationID > 0 {
|
||||
db = db.Where("kandangs.location_id = ?", uint(locationID))
|
||||
}
|
||||
|
||||
if areaID > 0 {
|
||||
db = db.Joins("JOIN locations ON locations.id = kandangs.location_id").
|
||||
Where("locations.area_id = ?", uint(areaID))
|
||||
}
|
||||
}
|
||||
|
||||
if filters.RealizationDate != "" {
|
||||
if realizationDate, err := utils.ParseDateString(filters.RealizationDate); err == nil {
|
||||
db = db.Where("DATE(expenses.realization_date) = ?", realizationDate)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := db.
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Order("expense_realizations.created_at DESC").
|
||||
Find(&realizations).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return realizations, total, nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||
@@ -188,7 +189,11 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||
}
|
||||
|
||||
createdBy := uint64(1) //todo get from auth
|
||||
actorID, err := middleware.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
createdBy := uint64(actorID)
|
||||
expense = &entity.Expense{
|
||||
ReferenceNumber: referenceNumber,
|
||||
PoNumber: req.PoNumber,
|
||||
@@ -360,6 +365,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||
}
|
||||
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||
return err
|
||||
}
|
||||
categoryChanged := false
|
||||
var newCategory string
|
||||
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||
@@ -404,6 +412,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
if ens.KandangId != nil {
|
||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||
if err != nil {
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||
}
|
||||
@@ -496,7 +507,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
}
|
||||
}
|
||||
|
||||
actorID := uint(1) // TODO: replace with authenticated user id
|
||||
actorID, err := middleware.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||
|
||||
approvalAction := entity.ApprovalActionUpdated
|
||||
@@ -543,7 +557,21 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Nonstocks")
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get expense for ID %d: %+v", id, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||
}
|
||||
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||
@@ -572,6 +600,20 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
||||
}
|
||||
|
||||
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Nonstocks")
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||
}
|
||||
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
@@ -655,7 +697,10 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID := uint(1) // TODO: replace with authenticated user id
|
||||
actorID, err := middleware.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
|
||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -712,7 +757,19 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Nonstocks")
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||
}
|
||||
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||
@@ -960,11 +1017,14 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
||||
}
|
||||
|
||||
actorID := uint(1) // TODO: replace with authenticated user id
|
||||
actorID, err := middleware.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||
}
|
||||
|
||||
var results []expenseDto.ExpenseDetailDTO
|
||||
|
||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||
@@ -1010,6 +1070,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||
}
|
||||
if approvalAction == entity.ApprovalActionApproved {
|
||||
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Nonstocks")
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||
}
|
||||
|
||||
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
@@ -1079,13 +1154,45 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
||||
return nil
|
||||
}
|
||||
|
||||
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||
// user, ok := authmiddleware.AuthenticatedUser(c)
|
||||
// if !ok || user == nil || user.Id == 0 {
|
||||
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
// }
|
||||
// return user.Id, nil
|
||||
// }
|
||||
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
||||
ctx context.Context,
|
||||
expense *entity.Expense,
|
||||
) error {
|
||||
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
||||
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return user.Id, nil
|
||||
// }
|
||||
seen := make(map[uint]struct{})
|
||||
|
||||
for _, ens := range expense.Nonstocks {
|
||||
// Field ini pointer, bisa nil
|
||||
if ens.ProjectFlockKandangId == nil || *ens.ProjectFlockKandangId == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pfkID := uint(*ens.ProjectFlockKandangId)
|
||||
if _, ok := seen[pfkID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[pfkID] = struct{}{}
|
||||
|
||||
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, pfkID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Project flock %d tidak ditemukan", pfkID),
|
||||
)
|
||||
}
|
||||
s.Log.Errorf("Failed to validate project flock %d for expense %d: %+v", pfkID, expense.Id, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||
}
|
||||
// ❗ RULE: kalau ClosedAt tidak nil → project sudah closing
|
||||
if pfk.ClosedAt != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,10 +33,8 @@ type ProductWarehouseDTO struct {
|
||||
|
||||
type AdjustmentRelationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
BeforeQuantity float64 `json:"before_quantity"`
|
||||
AfterQuantity float64 `json:"after_quantity"`
|
||||
Increase float64 `json:"increase"`
|
||||
Decrease float64 `json:"decrease"`
|
||||
Note string `json:"note,omitempty"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
@@ -104,12 +102,10 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
||||
|
||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
||||
return AdjustmentRelationDTO{
|
||||
Id: e.Id,
|
||||
// TransactionType: e.LoggableType,
|
||||
// Quantity: e.Q,
|
||||
// BeforeQuantity: e.BeforeQuantity,
|
||||
// AfterQuantity: e.AfterQuantity,
|
||||
Id: e.Id,
|
||||
Note: e.Notes,
|
||||
Increase: e.Increase,
|
||||
Decrease: e.Decrease,
|
||||
ProductWarehouseId: e.ProductWarehouseId,
|
||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
@@ -21,10 +21,11 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
productRepo := rproduct.NewProductRepository(db)
|
||||
|
||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
|
||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
AdjustmentRoutes(router, userService, adjustmentService)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
@@ -11,13 +16,10 @@ import (
|
||||
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AdjustmentService interface {
|
||||
@@ -27,22 +29,24 @@ type AdjustmentService interface {
|
||||
}
|
||||
|
||||
type adjustmentService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo productRepo.ProductRepository
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||
ProductRepo productRepo.ProductRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
|
||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService {
|
||||
return &adjustmentService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProductRepo: productRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +109,15 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||
}
|
||||
if !isProductWarehouseExist {
|
||||
|
||||
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newPW := &entity.ProductWarehouse{
|
||||
ProductId: uint(req.ProductID),
|
||||
WarehouseId: uint(req.WarehouseID),
|
||||
Quantity: 0,
|
||||
ProductId: uint(req.ProductID),
|
||||
WarehouseId: uint(req.WarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||
}
|
||||
|
||||
@@ -120,6 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||
}
|
||||
|
||||
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
ctx,
|
||||
uint(req.ProductID),
|
||||
uint(req.WarehouseID),
|
||||
)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||
}
|
||||
|
||||
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||
ctx,
|
||||
s.StockLogsRepository.DB(),
|
||||
[]uint{pw.Id},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||
if err != nil {
|
||||
@@ -170,6 +195,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return s.GetOne(c, createdLogId)
|
||||
}
|
||||
|
||||
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||
}
|
||||
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||
}
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||
}
|
||||
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||
}
|
||||
|
||||
return uint(projectFlockKandang.Id), nil
|
||||
}
|
||||
|
||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
||||
if err := s.Validate.Struct(query); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -178,7 +229,6 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
||||
|
||||
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||
}
|
||||
if query.WarehouseID > 0 && !isWarehousesExist {
|
||||
|
||||
+21
-6
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
|
||||
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error)
|
||||
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
@@ -93,7 +93,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con
|
||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||
Order("product_warehouses.created_at DESC")
|
||||
Order("product_warehouses.id DESC")
|
||||
|
||||
// preload relations so nested Product and Warehouse are populated
|
||||
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
||||
@@ -199,10 +199,21 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||
ctx context.Context,
|
||||
productID uint,
|
||||
warehouseID uint,
|
||||
projectFlockKandangID *uint,
|
||||
createdBy uint,
|
||||
) (uint, error) {
|
||||
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||
if err == nil {
|
||||
// Backfill project_flock_kandang_id when it's missing and caller provides one.
|
||||
if projectFlockKandangID != nil && (record.ProjectFlockKandangId == nil || *record.ProjectFlockKandangId == 0) {
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
Where("id = ?", record.Id).
|
||||
Update("project_flock_kandang_id", *projectFlockKandangID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
record.ProjectFlockKandangId = projectFlockKandangID
|
||||
}
|
||||
return record.Id, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -210,9 +221,10 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||
}
|
||||
|
||||
entity := &entity.ProductWarehouse{
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
Quantity: 0,
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
ProjectFlockKandangId: projectFlockKandangID,
|
||||
Quantity: 0,
|
||||
// CreatedBy: uint(createdBy),
|
||||
}
|
||||
// if entity.CreatedBy == 0 {
|
||||
@@ -258,7 +270,10 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||
Order("product_warehouses.id DESC").
|
||||
Preload("Product").Preload("Warehouse").
|
||||
Preload("Product").
|
||||
Preload("Product.ProductCategory").
|
||||
Preload("Product.Uom").
|
||||
Preload("Warehouse").
|
||||
Find(&productWarehouses).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -25,8 +27,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
|
||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
|
||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
TransferRoutes(router, userService, transferService)
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -35,9 +40,11 @@ type transferService struct {
|
||||
StockLogsRepository rStockLogs.StockLogRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
|
||||
return &transferService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -48,8 +55,11 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
||||
StockLogsRepository: stockLogsRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
@@ -87,13 +97,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
||||
|
||||
return transfers, total, nil
|
||||
|
||||
}
|
||||
|
||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||
var transfer entity.StockTransfer
|
||||
|
||||
// gunakan repo secara langsung
|
||||
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db)
|
||||
})
|
||||
@@ -112,7 +120,8 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
||||
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||
|
||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||
pwIDs := make([]uint, 0, len(req.Products))
|
||||
|
||||
for _, product := range req.Products {
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
||||
@@ -126,13 +135,22 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||
}
|
||||
pwIDs = append(pwIDs, sourcePW.Id)
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||
c.Context(),
|
||||
s.StockTransferRepo.DB(),
|
||||
pwIDs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
|
||||
deliveryQtyMap := make(map[uint]float64)
|
||||
for _, delivery := range req.Deliveries {
|
||||
for _, prod := range delivery.Products {
|
||||
@@ -140,7 +158,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// Cek: qty delivery tidak boleh melebihi qty di root
|
||||
for _, product := range req.Products {
|
||||
if deliveryQtyMap[product.ProductID] > product.ProductQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||
@@ -148,7 +165,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// cek suplier id caegory BOP cek by id
|
||||
for _, delivery := range req.Deliveries {
|
||||
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
|
||||
if err != nil {
|
||||
@@ -162,8 +178,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// Generate movement number
|
||||
// Format: PND-MBU-00001
|
||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get next movement number: %+v", err)
|
||||
@@ -181,17 +195,14 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
CreatedBy: uint64(actorID),
|
||||
}
|
||||
|
||||
// Save the transfer entity to the database
|
||||
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
// Insert header
|
||||
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
||||
|
||||
// insert ke details
|
||||
var details []*entity.StockTransferDetail
|
||||
for _, product := range req.Products {
|
||||
details = append(details, &entity.StockTransferDetail{
|
||||
@@ -206,7 +217,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
// Tambahkan proses insert delivery
|
||||
var deliveries []*entity.StockTransferDelivery
|
||||
for _, delivery := range req.Deliveries {
|
||||
deliveries = append(deliveries, &entity.StockTransferDelivery{
|
||||
@@ -214,7 +224,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
SupplierId: uint64(delivery.SupplierID),
|
||||
VehiclePlate: delivery.VehiclePlate,
|
||||
DriverName: delivery.DriverName,
|
||||
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses
|
||||
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
|
||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||
ShippingCostTotal: delivery.DeliveryCost,
|
||||
})
|
||||
@@ -223,7 +233,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||
return err
|
||||
}
|
||||
// tambahkan insert ke delivery items sebagai pivot
|
||||
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
@@ -251,9 +261,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
|
||||
for _, product := range req.Products {
|
||||
// Kurangi stok di gudang asal
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
|
||||
@@ -270,15 +278,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||
|
||||
// create stock log for decrease (source)
|
||||
// beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
||||
decreaseLog := &entity.StockLog{
|
||||
// TransactionType: entity.TransactionTypeDecrease,
|
||||
// Quantity: product.ProductQty,
|
||||
// BeforeQuantity: beforeQty,
|
||||
// AfterQuantity: sourcePW.Qty,
|
||||
// LogType: entity.LogTypeTransfer,
|
||||
// LogId: uint(entityTransfer.Id),
|
||||
Decrease: product.ProductQty,
|
||||
Notes: "",
|
||||
LoggableType: entity.LogTypeTransfer,
|
||||
@@ -291,7 +291,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return err
|
||||
}
|
||||
|
||||
// Tambah stok di gudang tujuan
|
||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
@@ -300,12 +299,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||
}
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
||||
ctx := c.Context()
|
||||
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
}
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||
@@ -313,7 +316,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
||||
}
|
||||
// Update stok di gudang tujuan
|
||||
|
||||
destPW.Quantity += product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
|
||||
@@ -321,13 +324,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||
|
||||
// create stock log for increase (destination)
|
||||
// beforeDestQty := destPW.Quantity - product.ProductQty
|
||||
increaseLog := &entity.StockLog{
|
||||
// TransactionType: entity.TransactionTypeIncrease,
|
||||
// Quantity: product.ProductQty,
|
||||
// BeforeQuantity: beforeDestQty,
|
||||
// AfterQuantity: destPW.Qty,
|
||||
Increase: product.ProductQty,
|
||||
LoggableType: entity.LogTypeTransfer,
|
||||
LoggableId: uint(entityTransfer.Id),
|
||||
@@ -339,7 +336,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
s.Log.Errorf("Failed to create stock log increase: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -350,10 +346,35 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
||||
}
|
||||
|
||||
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
|
||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||
}
|
||||
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||
}
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||
}
|
||||
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||
}
|
||||
|
||||
return uint(projectFlockKandang.Id), nil
|
||||
}
|
||||
|
||||
+6
-6
@@ -4,9 +4,9 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -23,7 +23,7 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
|
||||
}
|
||||
|
||||
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
query := &validation.DeliveryOrderQuery{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
||||
@@ -76,7 +76,7 @@ func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
req := new(validation.DeliveryOrderCreate)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
@@ -97,7 +97,7 @@ func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
req := new(validation.DeliveryOrderUpdate)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
+3
-3
@@ -3,9 +3,9 @@ package controller
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -1,38 +0,0 @@
|
||||
package delivery_orderss
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type DeliveryOrdersModule struct{}
|
||||
|
||||
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
marketingRepo := rMarketing.NewMarketingRepository(db)
|
||||
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
||||
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
// Register workflow steps for MARKETINGS approval
|
||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||
}
|
||||
|
||||
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package delivery_orderss
|
||||
|
||||
import (
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
||||
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
|
||||
ctrl := controller.NewDeliveryOrdersController(s)
|
||||
v1.Use(m.Auth(u))
|
||||
v1.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), ctrl.GetAll)
|
||||
v1.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), ctrl.GetOne)
|
||||
|
||||
// Sisanya di group /delivery-orders
|
||||
route := v1.Group("/delivery-orders")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Post("/",m.RequirePermissions(m.P_DeliveryCreateOne), ctrl.CreateOne)
|
||||
route.Patch("/:id",m.RequirePermissions(m.P_DeliveryUpdateOne), ctrl.UpdateOne)
|
||||
|
||||
}
|
||||
+12
-11
@@ -24,7 +24,7 @@ type MarketingListDTO struct {
|
||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||
SoDocs string `json:"so_docs"`
|
||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
||||
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -36,13 +36,14 @@ type MarketingDetailDTO struct {
|
||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||
SoDocs string `json:"so_docs"`
|
||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
||||
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
|
||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductDTO struct {
|
||||
Id uint `json:"id"`
|
||||
MarketingProductId uint `json:"marketing_product_id"`
|
||||
@@ -73,7 +74,7 @@ type DeliveryGroupDTO struct {
|
||||
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
||||
}
|
||||
|
||||
type MarketingProductDTO struct {
|
||||
type DeliveryMarketingProductDTO struct {
|
||||
Id uint `json:"id"`
|
||||
MarketingId uint `json:"marketing_id"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
@@ -95,14 +96,14 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
|
||||
func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO {
|
||||
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
||||
if e.ProductWarehouse.Id != 0 {
|
||||
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
||||
productWarehouse = &mapped
|
||||
}
|
||||
|
||||
return MarketingProductDTO{
|
||||
return DeliveryMarketingProductDTO{
|
||||
Id: e.Id,
|
||||
MarketingId: e.MarketingId,
|
||||
ProductWarehouseId: e.ProductWarehouseId,
|
||||
@@ -155,11 +156,11 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
|
||||
latestApproval = mapped
|
||||
}
|
||||
|
||||
var salesOrderProducts []MarketingProductDTO
|
||||
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||
if len(marketing.Products) > 0 {
|
||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
||||
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||
for i, product := range marketing.Products {
|
||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
||||
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,11 +196,11 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
|
||||
salesPerson = mapped
|
||||
}
|
||||
|
||||
var salesOrderProducts []MarketingProductDTO
|
||||
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||
if len(marketing.Products) > 0 {
|
||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
||||
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||
for i, product := range marketing.Products {
|
||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
||||
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,52 @@
|
||||
package marketing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type MarketingModule struct{}
|
||||
|
||||
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
RegisterRoutes(router, db, validate)
|
||||
// Initialize repositories
|
||||
marketingRepo := repository.NewMarketingRepository(db)
|
||||
marketingProductRepo := repository.NewMarketingProductRepository(db)
|
||||
marketingDeliveryProductRepo := repository.NewMarketingDeliveryProductRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
|
||||
// Initialize approval service
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
|
||||
// Register workflow steps for marketing approval
|
||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||
}
|
||||
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
// Initialize services
|
||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
// Register routes
|
||||
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MarketingRepository interface {
|
||||
repository.BaseRepository[entity.Marketing]
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetNextSequence(ctx context.Context) (uint, error)
|
||||
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
}
|
||||
|
||||
type MarketingRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Marketing]
|
||||
}
|
||||
|
||||
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
||||
return &MarketingRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
||||
var maxID uint
|
||||
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return maxID + 1, nil
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = r.DB()
|
||||
}
|
||||
|
||||
var soNumber string
|
||||
err := db.WithContext(ctx).
|
||||
Raw("SELECT generate_so_number()").
|
||||
Scan(&soNumber).Error
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate SO number: %w", err)
|
||||
}
|
||||
|
||||
return soNumber, nil
|
||||
}
|
||||
+84
@@ -5,6 +5,8 @@ import (
|
||||
|
||||
"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/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -13,6 +15,7 @@ type MarketingDeliveryProductRepository interface {
|
||||
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductRepositoryImpl struct {
|
||||
@@ -74,3 +77,84 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con
|
||||
|
||||
return &deliveryProduct, nil
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) {
|
||||
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||
var total int64
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Preload("MarketingProduct", func(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Marketing").
|
||||
Preload("Marketing.Customer").
|
||||
Preload("Marketing.SalesPerson").
|
||||
Preload("ProductWarehouse").
|
||||
Preload("ProductWarehouse.Product").
|
||||
Preload("ProductWarehouse.Warehouse")
|
||||
}).
|
||||
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||
Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id")
|
||||
|
||||
if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 {
|
||||
db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id")
|
||||
}
|
||||
|
||||
if filters.ProductId > 0 {
|
||||
db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id")
|
||||
}
|
||||
|
||||
if filters.WarehouseId > 0 {
|
||||
db = db.Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id")
|
||||
}
|
||||
|
||||
if filters.Search != "" {
|
||||
db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?",
|
||||
"%"+filters.Search+"%")
|
||||
}
|
||||
|
||||
if filters.CustomerId > 0 {
|
||||
db = db.Where("marketings.customer_id = ?", filters.CustomerId)
|
||||
}
|
||||
|
||||
if filters.SalesPersonId > 0 {
|
||||
db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId)
|
||||
}
|
||||
|
||||
if filters.MarketingId > 0 {
|
||||
db = db.Where("marketings.id = ?", filters.MarketingId)
|
||||
}
|
||||
|
||||
if filters.ProductId > 0 {
|
||||
db = db.Where("product_warehouses.product_id = ?", filters.ProductId)
|
||||
}
|
||||
|
||||
if filters.WarehouseId > 0 {
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId)
|
||||
}
|
||||
|
||||
if filters.ProjectFlockKandangId > 0 {
|
||||
db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId)
|
||||
}
|
||||
|
||||
if filters.DeliveryDate != "" {
|
||||
if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil {
|
||||
nextDate := deliveryDate.AddDate(0, 0, 1)
|
||||
db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := db.
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Order("marketing_delivery_products.id DESC").
|
||||
Find(&deliveryProducts).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return deliveryProducts, total, nil
|
||||
}
|
||||
@@ -1,27 +1,31 @@
|
||||
package marketing
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/controllers"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
|
||||
deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
group := router.Group("/marketing")
|
||||
func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrdersService service.SalesOrdersService, deliveryOrdersService service.DeliveryOrdersService) {
|
||||
salesOrdersCtrl := controller.NewSalesOrdersController(salesOrdersService)
|
||||
deliveryOrdersCtrl := controller.NewDeliveryOrdersController(deliveryOrdersService)
|
||||
|
||||
allModules := []modules.Module{
|
||||
salesOrderss.SalesOrdersModule{},
|
||||
deliveryOrderss.DeliveryOrdersModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
route := router.Group("/marketing")
|
||||
route.Use(m.Auth(userService))
|
||||
|
||||
for _, m := range allModules {
|
||||
m.RegisterRoutes(group, db, validate)
|
||||
}
|
||||
route.Get("/", deliveryOrdersCtrl.GetAll)
|
||||
route.Get("/:id", deliveryOrdersCtrl.GetOne)
|
||||
route.Delete("/:id", salesOrdersCtrl.DeleteOne)
|
||||
|
||||
route.Post("/sales-orders", salesOrdersCtrl.CreateOne)
|
||||
route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne)
|
||||
route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval)
|
||||
|
||||
route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll)
|
||||
route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne)
|
||||
route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne)
|
||||
route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package sales_orders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
type SalesOrdersModule struct{}
|
||||
|
||||
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
marketingRepo := rSalesOrders.NewMarketingRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
||||
|
||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||
}
|
||||
|
||||
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
SalesOrdersRoutes(router, userService, salesOrdersService)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type MarketingRepository interface {
|
||||
repository.BaseRepository[entity.Marketing]
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetNextSequence(ctx context.Context) (uint, error)
|
||||
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
}
|
||||
|
||||
type MarketingRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Marketing]
|
||||
}
|
||||
|
||||
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
||||
return &MarketingRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
||||
var maxID uint
|
||||
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return maxID + 1, nil
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||
return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding)
|
||||
}
|
||||
|
||||
func parseNumericSuffix(value, prefix string) (int, bool) {
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
return 0, false
|
||||
}
|
||||
suffix := strings.TrimPrefix(value, prefix)
|
||||
if suffix == "" {
|
||||
return 0, false
|
||||
}
|
||||
trimmed := strings.TrimLeft(suffix, "0")
|
||||
if trimmed == "" {
|
||||
trimmed = "0"
|
||||
}
|
||||
number, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return number, true
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&entity.Marketing{}).
|
||||
Where(fmt.Sprintf("%s = ?", column), value).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
|
||||
|
||||
db := tx
|
||||
if db == nil {
|
||||
db = r.DB()
|
||||
}
|
||||
|
||||
var values []string
|
||||
err := db.WithContext(ctx).
|
||||
Model(&entity.Marketing{}).
|
||||
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
|
||||
Select(column).
|
||||
Order(fmt.Sprintf("%s DESC", column)).
|
||||
Limit(20).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Pluck(column, &values).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
next := 1
|
||||
for _, value := range values {
|
||||
if number, ok := parseNumericSuffix(value, prefix); ok {
|
||||
next = number + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const maxAttempts = 20
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
|
||||
exists, err := r.numberExists(ctx, db, column, candidate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return candidate, nil
|
||||
}
|
||||
next++
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unable to generate unique %s", column)
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package sales_orders
|
||||
|
||||
import (
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
||||
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
|
||||
ctrl := controller.NewSalesOrdersController(s)
|
||||
v1.Use(m.Auth(u))
|
||||
v1.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), ctrl.DeleteOne)
|
||||
route := v1.Group("/sales-orders")
|
||||
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||
|
||||
route.Post("/",m.RequirePermissions(m.P_SalesOrderCreateOne), ctrl.CreateOne)
|
||||
route.Patch("/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), ctrl.UpdateOne)
|
||||
|
||||
route.Post("/approvals",m.RequirePermissions(m.P_SalesOrderApproval), ctrl.Approval)
|
||||
}
|
||||
+24
-9
@@ -11,9 +11,9 @@ import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
)
|
||||
|
||||
type DeliveryOrdersService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error)
|
||||
GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
||||
}
|
||||
|
||||
type deliveryOrdersService struct {
|
||||
@@ -85,7 +85,7 @@ func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketin
|
||||
return &responseDTO, nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) {
|
||||
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDeta
|
||||
return &responseDTO, nil
|
||||
}
|
||||
|
||||
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) {
|
||||
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -222,6 +222,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -285,7 +293,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) {
|
||||
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -319,6 +327,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
if foundMarketingProduct == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||
}
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||
if err != nil {
|
||||
+76
-17
@@ -11,9 +11,11 @@ import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
@@ -32,24 +34,29 @@ type SalesOrdersService interface {
|
||||
}
|
||||
|
||||
type salesOrdersService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
MarketingRepo repository.MarketingRepository
|
||||
CustomerRepo customerRepo.CustomerRepository
|
||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||
UserRepo userRepo.UserRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
MarketingRepo repository.MarketingRepository
|
||||
CustomerRepo customerRepo.CustomerRepository
|
||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||
UserRepo userRepo.UserRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
}
|
||||
|
||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService {
|
||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository,
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
||||
return &salesOrdersService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
MarketingRepo: marketingRepo,
|
||||
CustomerRepo: customerRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
UserRepo: userRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
MarketingRepo: marketingRepo,
|
||||
CustomerRepo: customerRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
UserRepo: userRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +147,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if len(req.MarketingProducts) > 0 {
|
||||
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||
for _, product := range req.MarketingProducts {
|
||||
if product.ProductWarehouseId != 0 {
|
||||
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
||||
}
|
||||
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||
}
|
||||
|
||||
}
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +228,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(req.MarketingProducts) > 0 {
|
||||
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||
for _, item := range req.MarketingProducts {
|
||||
if item.ProductWarehouseId != 0 {
|
||||
pwIDs = append(pwIDs, item.ProductWarehouseId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
@@ -367,7 +394,18 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||
}
|
||||
if len(marketing.Products) > 0 {
|
||||
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||
for _, p := range marketing.Products {
|
||||
if p.ProductWarehouseId != 0 {
|
||||
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||
@@ -458,6 +496,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
||||
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
||||
}
|
||||
}
|
||||
marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Products")
|
||||
})
|
||||
if mErr != nil {
|
||||
if errors.Is(mErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation")
|
||||
}
|
||||
|
||||
if len(marketing.Products) > 0 {
|
||||
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||
for _, p := range marketing.Products {
|
||||
if p.ProductWarehouseId != 0 {
|
||||
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||
}
|
||||
}
|
||||
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
+4
-4
@@ -11,22 +11,22 @@ type DeliveryProduct struct {
|
||||
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
type DeliveryOrderCreate struct {
|
||||
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
type DeliveryOrderUpdate struct {
|
||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
type DeliveryOrderQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
type DeliveryOrderApprove struct {
|
||||
Action string `json:"action" validate:"required_strict"`
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
@@ -1,11 +1,11 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"time"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
@@ -18,13 +18,14 @@ type NonstockRelationDTO struct {
|
||||
}
|
||||
|
||||
type NonstockListDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Flags []string `json:"flags"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Flags []string `json:"flags"`
|
||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NonstockDetailDTO struct {
|
||||
@@ -76,6 +77,7 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
|
||||
Name: e.Name,
|
||||
Flags: flags,
|
||||
Uom: uomRef,
|
||||
Suppliers: toNonstockSupplierDTOs(e.NonstockSuppliers),
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedUser: createdUser,
|
||||
@@ -95,3 +97,23 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
||||
NonstockListDTO: ToNonstockListDTO(e),
|
||||
}
|
||||
}
|
||||
|
||||
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.Supplier.Id == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||
}
|
||||
|
||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type ProjectChickinRepository interface {
|
||||
repository.BaseRepository[entity.ProjectChickin]
|
||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error)
|
||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
@@ -40,6 +41,16 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
|
||||
return &chickin, nil
|
||||
}
|
||||
|
||||
func (r *ChickinRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error) {
|
||||
var chickins []entity.ProjectChickin
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
|
||||
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||
Order("project_chickins.created_at DESC").
|
||||
Find(&chickins).Error
|
||||
return chickins, err
|
||||
}
|
||||
|
||||
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||
var chickins []entity.ProjectChickin
|
||||
err := r.db.WithContext(ctx).
|
||||
|
||||
@@ -190,14 +190,14 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||
}
|
||||
|
||||
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||
}
|
||||
|
||||
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
for _, chickin := range newChikins {
|
||||
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
|
||||
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -218,9 +218,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
if latest == nil {
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
utils.ApprovalWorkflowChickin,
|
||||
projectFlockKandang.Id,
|
||||
utils.ProjectFlockKandangStepPengajuan,
|
||||
utils.ChickinStepPengajuan,
|
||||
&approvalAction,
|
||||
actorID,
|
||||
nil); err != nil {
|
||||
@@ -228,12 +228,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||
}
|
||||
}
|
||||
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||
} else if latest.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||
if _, err := approvalSvcTx.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
utils.ApprovalWorkflowChickin,
|
||||
projectFlockKandang.Id,
|
||||
utils.ProjectFlockKandangStepPengajuan,
|
||||
utils.ChickinStepPengajuan,
|
||||
&approvalAction,
|
||||
actorID,
|
||||
nil); err != nil {
|
||||
@@ -388,7 +388,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||
@@ -396,14 +396,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
if latestApproval == nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
||||
}
|
||||
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||
if latestApproval.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
||||
}
|
||||
}
|
||||
|
||||
step := utils.ProjectFlockKandangStepPengajuan
|
||||
step := utils.ChickinStepPengajuan
|
||||
if action == entity.ApprovalActionApproved {
|
||||
step = utils.ProjectFlockKandangStepDisetujui
|
||||
step = utils.ChickinStepDisetujui
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
@@ -415,7 +415,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
for _, approvableID := range approvableIDs {
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
utils.ApprovalWorkflowChickin,
|
||||
approvableID,
|
||||
step,
|
||||
&action,
|
||||
@@ -498,7 +498,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
for _, chickin := range chickins {
|
||||
|
||||
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
||||
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
|
||||
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -600,7 +600,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
|
||||
if chickin.ProductWarehouseId != targetPW.Id {
|
||||
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
||||
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
|
||||
"qty": gorm.Expr("qty - ?", quantityToConvert),
|
||||
}, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
||||
@@ -610,7 +610,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
||||
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
|
||||
"qty": gorm.Expr("qty + ?", quantityToConvert),
|
||||
}, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
||||
|
||||
+54
@@ -84,3 +84,57 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
|
||||
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
req := new(validation.Closing)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.ProjectFlockKandangService.Closing(c, uint(id), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
detail, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, result.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
detailDTO := dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*detail, availableQtys, productWarehouses)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Status closing kandang diperbarui",
|
||||
Data: fiber.Map{
|
||||
"detail": detailDTO,
|
||||
"approval": detailDTO.Approval,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProjectFlockKandangController) CheckClosing(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil || id <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.ProjectFlockKandangService.CheckClosing(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Cek persyaratan closing kandang",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -28,15 +29,17 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
// register workflow steps for project flock kandang approvals
|
||||
// register workflow steps for chickin approvals
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||
}
|
||||
|
||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
|
||||
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||
|
||||
@@ -16,5 +16,8 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
|
||||
route.Use(m.Auth(u))
|
||||
route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne)
|
||||
|
||||
// route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing)
|
||||
// route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing)
|
||||
route.Post("/:id/closing", ctrl.Closing)
|
||||
route.Get("/:id/closing/check", ctrl.CheckClosing)
|
||||
}
|
||||
|
||||
+393
-11
@@ -2,47 +2,84 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProjectFlockKandangService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
||||
CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
|
||||
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
|
||||
GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
|
||||
}
|
||||
|
||||
// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity
|
||||
|
||||
type projectFlockKandangService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseRepo expenseRepo.ExpenseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||
KandangRepo kandangRepo.KandangRepository
|
||||
}
|
||||
|
||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||
type ClosingCheckResult struct {
|
||||
UnfinishedExpenses int64 `json:"unfinished_expenses"`
|
||||
StockRemaining []StockRemainingDetail `json:"stock_remaining"`
|
||||
Expenses []ExpenseSummary `json:"expenses"`
|
||||
}
|
||||
|
||||
type StockRemainingDetail struct {
|
||||
FlagName string `json:"flag_name"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductCategory string `json:"product_category"`
|
||||
Uom string `json:"uom"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
type ExpenseSummary struct {
|
||||
Id uint64 `json:"id"`
|
||||
PoNumber string `json:"po_number"`
|
||||
Category string `json:"category"`
|
||||
Total float64 `json:"total"`
|
||||
Status string `json:"status"`
|
||||
StepName string `json:"step_name"`
|
||||
Step uint16 `json:"step"`
|
||||
Reference string `json:"reference_number"`
|
||||
}
|
||||
|
||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||
return &projectFlockKandangService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseRepo: expenseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
PopulationRepo: populationRepo,
|
||||
KandangRepo: kandangRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +203,351 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) {
|
||||
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unfinished int64
|
||||
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||
count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unfinished = count
|
||||
}
|
||||
|
||||
stockRemain := make([]StockRemainingDetail, 0)
|
||||
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||
if werr != nil {
|
||||
return nil, werr
|
||||
}
|
||||
|
||||
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||
if pwErr != nil {
|
||||
return nil, pwErr
|
||||
}
|
||||
|
||||
for _, pw := range productWarehouses {
|
||||
if pw.Quantity > 0 {
|
||||
category := ""
|
||||
if pw.Product.ProductCategory.Id != 0 {
|
||||
category = pw.Product.ProductCategory.Name
|
||||
}
|
||||
uomName := ""
|
||||
if pw.Product.Uom.Id != 0 {
|
||||
uomName = pw.Product.Uom.Name
|
||||
}
|
||||
stockRemain = append(stockRemain, StockRemainingDetail{
|
||||
FlagName: string(flagName),
|
||||
ProductWarehouseId: pw.Id,
|
||||
ProductId: pw.ProductId,
|
||||
ProductName: pw.Product.Name,
|
||||
ProductCategory: category,
|
||||
Uom: uomName,
|
||||
Quantity: pw.Quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expenseSummaries := make([]ExpenseSummary, 0)
|
||||
if s.ExpenseRepo != nil {
|
||||
var expenses []entity.Expense
|
||||
if err := s.ExpenseRepo.DB().WithContext(c.Context()).
|
||||
Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)).
|
||||
Preload("Nonstocks").
|
||||
Find(&expenses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(expenses) > 0 && s.ApprovalSvc != nil {
|
||||
ids := make([]uint, 0, len(expenses))
|
||||
for _, e := range expenses {
|
||||
ids = append(ids, uint(e.Id))
|
||||
}
|
||||
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil)
|
||||
if err == nil {
|
||||
for i := range expenses {
|
||||
if latest, ok := latestMap[uint(expenses[i].Id)]; ok {
|
||||
expenses[i].LatestApproval = latest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, exp := range expenses {
|
||||
total := 0.0
|
||||
for _, ns := range exp.Nonstocks {
|
||||
total += ns.Qty * ns.Price
|
||||
}
|
||||
|
||||
status := "Pending"
|
||||
stepName := ""
|
||||
stepNum := uint16(0)
|
||||
if exp.LatestApproval != nil {
|
||||
stepName = exp.LatestApproval.StepName
|
||||
stepNum = exp.LatestApproval.StepNumber
|
||||
if exp.LatestApproval.Action != nil {
|
||||
status = string(*exp.LatestApproval.Action)
|
||||
} else if stepName != "" {
|
||||
status = stepName
|
||||
}
|
||||
}
|
||||
|
||||
expenseSummaries = append(expenseSummaries, ExpenseSummary{
|
||||
Id: exp.Id,
|
||||
PoNumber: exp.PoNumber,
|
||||
Category: exp.Category,
|
||||
Total: total,
|
||||
Status: status,
|
||||
StepName: stepName,
|
||||
Step: stepNum,
|
||||
Reference: exp.ReferenceNumber,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &ClosingCheckResult{
|
||||
UnfinishedExpenses: unfinished,
|
||||
StockRemaining: stockRemain,
|
||||
Expenses: expenseSummaries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
|
||||
func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
|
||||
if id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return pfk.ClosedAt, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.ApprovalSvc != nil {
|
||||
latest, aerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||
if aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||
}
|
||||
if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) && latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||
}
|
||||
}
|
||||
|
||||
action := strings.ToLower(strings.TrimSpace(req.Action))
|
||||
now := time.Now()
|
||||
|
||||
switch action {
|
||||
case "close":
|
||||
if pfk.ClosedAt != nil {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed")
|
||||
}
|
||||
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||
unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unfinished > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Masih ada expense belum selesai untuk kandang ini")
|
||||
}
|
||||
}
|
||||
|
||||
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||
if werr != nil {
|
||||
return nil, werr
|
||||
}
|
||||
|
||||
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||
if pwErr != nil {
|
||||
return nil, pwErr
|
||||
}
|
||||
|
||||
for _, pw := range productWarehouses {
|
||||
if pw.Quantity > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok %s masih tersedia (product warehouse %d: %.2f)", flagName, pw.Id, pw.Quantity))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeTime := now
|
||||
if req.ClosedDate != nil {
|
||||
parsed, perr := utils.ParseDateString(strings.TrimSpace(*req.ClosedDate))
|
||||
if perr != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "closed_date tidak valid, gunakan format YYYY-MM-DD")
|
||||
}
|
||||
closeTime = parsed
|
||||
}
|
||||
if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.KandangRepo != nil {
|
||||
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||
c.Context(),
|
||||
[]uint{pfk.KandangId},
|
||||
utils.KandangStatusNonActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if s.ApprovalSvc != nil {
|
||||
closeAction := entity.ApprovalActionApproved
|
||||
// Hindari duplikasi jika approval terakhir sudah Closed + Approved
|
||||
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
shouldCreate := true
|
||||
if latestPFK != nil &&
|
||||
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepClosed) &&
|
||||
latestPFK.Action != nil && *latestPFK.Action == closeAction {
|
||||
shouldCreate = false
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
id,
|
||||
utils.ProjectFlockKandangStepClosed,
|
||||
&closeAction,
|
||||
actorID,
|
||||
nil,
|
||||
); aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
}
|
||||
|
||||
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
|
||||
pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId)
|
||||
if ferr != nil {
|
||||
return nil, ferr
|
||||
}
|
||||
allClosed := true
|
||||
for _, item := range pfks {
|
||||
if item.ClosedAt == nil {
|
||||
allClosed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allClosed {
|
||||
latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlock,
|
||||
pfk.ProjectFlockId,
|
||||
utils.ProjectFlockStepSelesai,
|
||||
&closeAction,
|
||||
actorID,
|
||||
nil,
|
||||
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||
return nil, aerr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "unclose":
|
||||
if pfk.ClosedAt == nil {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Kandang belum closed")
|
||||
}
|
||||
openNewer, err := s.Repository.HasOpenNewerPeriod(c.Context(), pfk.KandangId, pfk.Period, &pfk.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if openNewer {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Tidak dapat un-close: ada periode yang sedang berjalan")
|
||||
}
|
||||
if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.KandangRepo != nil {
|
||||
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||
c.Context(),
|
||||
[]uint{pfk.KandangId},
|
||||
utils.KandangStatusActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if s.ApprovalSvc != nil {
|
||||
reopenAction := entity.ApprovalActionUpdated
|
||||
// Hindari duplikasi jika approval terakhir sudah Disetujui + Updated
|
||||
latestPFK, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
shouldCreate := true
|
||||
if latestPFK != nil &&
|
||||
latestPFK.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) &&
|
||||
latestPFK.Action != nil && *latestPFK.Action == reopenAction {
|
||||
shouldCreate = false
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
id,
|
||||
utils.ProjectFlockKandangStepDisetujui,
|
||||
&reopenAction,
|
||||
actorID,
|
||||
nil,
|
||||
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||
return nil, aerr
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
|
||||
}
|
||||
|
||||
return s.Repository.GetByID(c.Context(), id)
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||
availableQty := productWarehouse.Quantity
|
||||
|
||||
|
||||
+5
@@ -22,3 +22,8 @@ type Query struct {
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
||||
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Closing struct {
|
||||
Action string `json:"action" validate:"required,oneof=close unclose"`
|
||||
ClosedDate *string `json:"closed_date,omitempty"`
|
||||
}
|
||||
@@ -165,33 +165,6 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.ProjectflockService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update projectflock successfully",
|
||||
Data: dto.ToProjectFlockListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
@@ -308,7 +281,6 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||
dtoResult.AvailableQuantity = float64(availableStock)
|
||||
|
||||
// populate available quantity for each kandang inside project_flock
|
||||
if dtoResult.ProjectFlock != nil {
|
||||
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||
kand := &dtoResult.ProjectFlock.Kandangs[i]
|
||||
@@ -319,7 +291,7 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||
kand.AvailableQuantity = q
|
||||
}
|
||||
}
|
||||
// remove inner kandangs from project_flock to avoid duplication
|
||||
|
||||
dtoResult.ProjectFlock.Kandangs = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
@@ -8,6 +10,7 @@ import (
|
||||
|
||||
type ProjectBudgetRepository interface {
|
||||
repository.BaseRepository[entity.ProjectBudget]
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error)
|
||||
}
|
||||
|
||||
type ProjectBudgetRepositoryImpl struct {
|
||||
@@ -21,3 +24,13 @@ func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error) {
|
||||
var budgets []entity.ProjectBudget
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Preload("Nonstock").
|
||||
Preload("Nonstock.Uom").
|
||||
Find(&budgets).Error
|
||||
return budgets, err
|
||||
}
|
||||
|
||||
+65
-25
@@ -3,6 +3,7 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -14,18 +15,21 @@ type ProjectFlockKandangRepository interface {
|
||||
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
||||
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||
UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error
|
||||
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
||||
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
||||
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
|
||||
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
DB() *gorm.DB
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
}
|
||||
|
||||
type projectFlockKandangRepositoryImpl struct {
|
||||
@@ -75,6 +79,16 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) {
|
||||
var records []entity.ProjectFlockKandang
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
||||
var records []entity.ProjectFlockKandang
|
||||
var total int64
|
||||
@@ -104,10 +118,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
|
||||
AND "approvals"."approvable_type" = ?
|
||||
AND LOWER("approvals"."step_name") = LOWER(?)
|
||||
AND "approvals"."id" IN (
|
||||
SELECT "id" FROM "approvals"
|
||||
WHERE "approvable_id" = "project_flock_kandangs"."id"
|
||||
AND "approvable_type" = ?
|
||||
ORDER BY "action_at" DESC
|
||||
SELECT "approvals"."id" FROM "approvals"
|
||||
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
|
||||
AND "approvals"."approvable_type" = ?
|
||||
ORDER BY "approvals"."id" DESC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
@@ -223,32 +237,57 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
latestApprovalSubQuery := r.db.
|
||||
Table("approvals").
|
||||
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
|
||||
Where("approvable_type = ?", "PROJECT_FLOCKS").
|
||||
Order("approvable_id, id DESC")
|
||||
|
||||
var pfkID uint
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||
Joins(`
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||
ORDER BY approvable_id, action_at DESC
|
||||
) latest_approval ON latest_approval.approvable_id = project_flocks.id
|
||||
`).
|
||||
Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
|
||||
Where("project_flock_kandangs.kandang_id = ?", kandangID).
|
||||
Where("project_flock_kandangs.closed_at IS NULL").
|
||||
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
||||
Order("project_flock_kandangs.id DESC").
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
Preload("ProjectFlock.CreatedUser").
|
||||
Preload("ProjectFlock.Kandangs").
|
||||
Preload("ProjectFlock.KandangHistory").
|
||||
Preload("Kandang").
|
||||
First(record).Error; err != nil {
|
||||
Limit(1).
|
||||
Pluck("project_flock_kandangs.id", &pfkID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
|
||||
if pfkID == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return r.GetByID(ctx, pfkID)
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.ProjectFlockKandang{}).
|
||||
Where("id = ?", id).
|
||||
Update("closed_at", t).Error
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) {
|
||||
if kandangID == 0 {
|
||||
return false, nil
|
||||
}
|
||||
q := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("kandang_id = ?", kandangID).
|
||||
Where("period > ?", currentPeriod).
|
||||
Where("closed_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
var count int64
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||
@@ -269,7 +308,8 @@ func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx
|
||||
}
|
||||
q := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("kandang_id IN ?", kandangIDs)
|
||||
Where("kandang_id IN ?", kandangIDs).
|
||||
Where("closed_at IS NULL")
|
||||
if exceptProjectID != nil {
|
||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
||||
route.Get("/",m.RequirePermissions(m.P_ProjectFlockGetAll),ctrl.GetAll)
|
||||
route.Post("/",m.RequirePermissions(m.P_ProjectFlockCreate), ctrl.CreateOne)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockGetOne), ctrl.GetOne)
|
||||
route.Patch("/:id",m.RequirePermissions(m.P_ProjectFlockUpdate), ctrl.UpdateOne)
|
||||
route.Delete("/:id",m.RequirePermissions(m.P_ProjectFlockGetAll), ctrl.DeleteOne)
|
||||
route.Get("/kandangs/lookup",m.RequirePermissions(m.P_ProjectFlockLookup), ctrl.LookupProjectFlockKandang)
|
||||
route.Post("/approvals",m.RequirePermissions(m.P_ProjectFlockApprove), ctrl.Approval)
|
||||
|
||||
@@ -34,7 +34,6 @@ type ProjectflockService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
|
||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||
@@ -255,6 +254,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var location entity.Location
|
||||
if err := s.Repository.DB().WithContext(c.Context()).
|
||||
Where("id = ? AND area_id = ?", req.LocationId, req.AreaId).
|
||||
First(&location).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi")
|
||||
}
|
||||
|
||||
canonicalBase := baseName
|
||||
if s.FlockRepo != nil {
|
||||
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
||||
@@ -348,365 +357,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return s.getOneEntityOnly(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
updateBody := make(map[string]any)
|
||||
hasBodyChanges := false
|
||||
var relationChecks []commonSvc.RelationCheck
|
||||
existingBase := pfutils.DeriveBaseName(existing.FlockName)
|
||||
targetBaseName := existingBase
|
||||
needFlockNameRegenerate := false
|
||||
|
||||
if req.FlockName != nil {
|
||||
trimmed := strings.TrimSpace(*req.FlockName)
|
||||
if trimmed == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
||||
}
|
||||
canonicalBase := trimmed
|
||||
if s.FlockRepo != nil {
|
||||
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonicalBase = flockEntity.Name
|
||||
}
|
||||
if !strings.EqualFold(canonicalBase, existingBase) {
|
||||
needFlockNameRegenerate = true
|
||||
targetBaseName = canonicalBase
|
||||
hasBodyChanges = true
|
||||
}
|
||||
}
|
||||
if req.AreaId != nil {
|
||||
updateBody["area_id"] = *req.AreaId
|
||||
hasBodyChanges = true
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Area",
|
||||
ID: req.AreaId,
|
||||
Exists: s.Repository.AreaExists,
|
||||
})
|
||||
}
|
||||
if req.Category != nil {
|
||||
cat := strings.ToUpper(*req.Category)
|
||||
if !utils.IsValidProjectFlockCategory(cat) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
||||
}
|
||||
|
||||
updateBody["category"] = cat
|
||||
}
|
||||
if req.FcrId != nil {
|
||||
updateBody["fcr_id"] = *req.FcrId
|
||||
hasBodyChanges = true
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "FCR",
|
||||
ID: req.FcrId,
|
||||
Exists: s.Repository.FcrExists,
|
||||
})
|
||||
}
|
||||
if req.LocationId != nil {
|
||||
updateBody["location_id"] = *req.LocationId
|
||||
hasBodyChanges = true
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Location",
|
||||
ID: req.LocationId,
|
||||
Exists: s.Repository.LocationExists,
|
||||
})
|
||||
}
|
||||
|
||||
if len(relationChecks) > 0 {
|
||||
if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var newKandangIDs []uint
|
||||
hasKandangChanges := false
|
||||
if req.KandangIds != nil {
|
||||
hasKandangChanges = true
|
||||
if len(req.KandangIds) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
|
||||
}
|
||||
newKandangIDs = uniqueUintSlice(req.KandangIds)
|
||||
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
||||
}
|
||||
if len(kandangs) != len(newKandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
targetLocationID := existing.LocationId
|
||||
if req.LocationId != nil && *req.LocationId > 0 {
|
||||
targetLocationID = *req.LocationId
|
||||
}
|
||||
for _, kandang := range kandangs {
|
||||
if kandang.LocationId != targetLocationID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
|
||||
}
|
||||
}
|
||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||
} else if linked {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hasChanges := hasBodyChanges || hasKandangChanges
|
||||
if !hasChanges {
|
||||
return s.getOneEntityOnly(c, id)
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||
|
||||
baseForGeneration := targetBaseName
|
||||
if strings.TrimSpace(baseForGeneration) == "" {
|
||||
baseForGeneration = existingBase
|
||||
}
|
||||
if strings.TrimSpace(baseForGeneration) == "" {
|
||||
baseForGeneration = strings.TrimSpace(existing.FlockName)
|
||||
}
|
||||
|
||||
if needFlockNameRegenerate {
|
||||
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateBody["flock_name"] = newName
|
||||
}
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if req.KandangIds != nil {
|
||||
existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
|
||||
for _, k := range existing.Kandangs {
|
||||
existingIDs[k.Id] = struct{}{}
|
||||
}
|
||||
newSet := make(map[uint]struct{}, len(newKandangIDs))
|
||||
for _, kid := range newKandangIDs {
|
||||
newSet[kid] = struct{}{}
|
||||
}
|
||||
|
||||
var toDetach []uint
|
||||
for kid := range existingIDs {
|
||||
if _, ok := newSet[kid]; !ok {
|
||||
toDetach = append(toDetach, kid)
|
||||
}
|
||||
}
|
||||
|
||||
var toAttach []uint
|
||||
for kid := range newSet {
|
||||
if _, ok := existingIDs[kid]; !ok {
|
||||
toAttach = append(toAttach, kid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toDetach) > 0 {
|
||||
if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAttach) > 0 {
|
||||
currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
periods := make(map[uint]int, len(toAttach))
|
||||
if currentPeriod > 0 {
|
||||
for _, kid := range toAttach {
|
||||
periods[kid] = currentPeriod
|
||||
}
|
||||
} else {
|
||||
periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasChanges {
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
if approvalSvc != nil {
|
||||
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shouldRecordUpdate := latestBeforeReset == nil ||
|
||||
latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
|
||||
latestBeforeReset.Action == nil ||
|
||||
(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)
|
||||
|
||||
if shouldRecordUpdate {
|
||||
action := entity.ApprovalActionUpdated
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlock,
|
||||
id,
|
||||
utils.ProjectFlockStepPengajuan,
|
||||
&action,
|
||||
actorID,
|
||||
nil,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
|
||||
}
|
||||
|
||||
return s.getOneEntityOnly(c, id)
|
||||
}
|
||||
|
||||
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var action entity.ApprovalAction
|
||||
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||
case string(entity.ApprovalActionRejected):
|
||||
action = entity.ApprovalActionRejected
|
||||
case string(entity.ApprovalActionApproved):
|
||||
action = entity.ApprovalActionApproved
|
||||
default:
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||
}
|
||||
|
||||
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
||||
if len(approvableIDs) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||
}
|
||||
|
||||
step := utils.ProjectFlockStepPengajuan
|
||||
if action == entity.ApprovalActionApproved {
|
||||
step = utils.ProjectFlockStepAktif
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
||||
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlock,
|
||||
approvableID,
|
||||
step,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch action {
|
||||
case entity.ApprovalActionApproved:
|
||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||
c.Context(),
|
||||
approvableID,
|
||||
utils.KandangStatusActive,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
case entity.ApprovalActionRejected:
|
||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||
c.Context(),
|
||||
approvableID,
|
||||
utils.KandangStatusNonActive,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||
}
|
||||
|
||||
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
||||
for _, approvableID := range approvableIDs {
|
||||
project, err := s.getOneEntityOnly(c, approvableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated = append(updated, *project)
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -827,6 +477,27 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// getProjectFlockClosingDate mengembalikan tanggal closing Project Flock jika sudah mencapai step SELESAI (Approved).
|
||||
// func (s projectflockService) getProjectFlockClosingDate(ctx context.Context, projectFlockID uint) (*time.Time, error) {
|
||||
// if projectFlockID == 0 || s.ApprovalSvc == nil {
|
||||
// return nil, nil
|
||||
// }
|
||||
|
||||
// latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowProjectFlock, projectFlockID, nil)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||
// return nil, nil
|
||||
// }
|
||||
// if latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||
// return nil, nil
|
||||
// }
|
||||
|
||||
// t := latest.ActionAt
|
||||
// return &t, nil
|
||||
// }
|
||||
|
||||
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
||||
if len(projectIDs) == 0 {
|
||||
return map[uint]int{}, nil
|
||||
@@ -834,6 +505,133 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint)
|
||||
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
||||
}
|
||||
|
||||
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var action entity.ApprovalAction
|
||||
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||
case string(entity.ApprovalActionRejected):
|
||||
action = entity.ApprovalActionRejected
|
||||
case string(entity.ApprovalActionApproved):
|
||||
action = entity.ApprovalActionApproved
|
||||
default:
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||
}
|
||||
|
||||
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
||||
if len(approvableIDs) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||
}
|
||||
|
||||
step := utils.ProjectFlockStepPengajuan
|
||||
if action == entity.ApprovalActionApproved {
|
||||
step = utils.ProjectFlockStepAktif
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
||||
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
||||
projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlock,
|
||||
approvableID,
|
||||
step,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch action {
|
||||
case entity.ApprovalActionApproved:
|
||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||
c.Context(),
|
||||
approvableID,
|
||||
utils.KandangStatusActive,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pfk := range pfks {
|
||||
latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil)
|
||||
if lerr != nil {
|
||||
return lerr
|
||||
}
|
||||
if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) {
|
||||
continue
|
||||
}
|
||||
if _, aerr := approvalSvc.CreateApproval(
|
||||
c.Context(),
|
||||
utils.ApprovalWorkflowProjectFlockKandang,
|
||||
pfk.Id,
|
||||
utils.ProjectFlockKandangStepDisetujui,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||
return aerr
|
||||
}
|
||||
}
|
||||
case entity.ApprovalActionRejected:
|
||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||
c.Context(),
|
||||
approvableID,
|
||||
utils.KandangStatusNonActive,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||
}
|
||||
|
||||
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
||||
for _, approvableID := range approvableIDs {
|
||||
project, err := s.getOneEntityOnly(c, approvableID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated = append(updated, *project)
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
||||
if locationID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
||||
|
||||
@@ -10,15 +10,6 @@ type Create struct {
|
||||
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
|
||||
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
Category *string `json:"category,omitempty" validate:"omitempty"`
|
||||
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package recordings
|
||||
|
||||
const (
|
||||
PermissionRecordingRead = "recording.read"
|
||||
PermissionRecordingCreate = "recording.write"
|
||||
PermissionRecordingUpdate = "recording.update"
|
||||
PermissionRecordingDelete = "recording.delete"
|
||||
)
|
||||
@@ -14,11 +14,12 @@ import (
|
||||
)
|
||||
|
||||
type PurchaseRelationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
PrNumber string `json:"pr_number"`
|
||||
PoNumber *string `json:"po_number"`
|
||||
PoDate *time.Time `json:"po_date"`
|
||||
Notes *string `json:"notes"`
|
||||
Id uint `json:"id"`
|
||||
PrNumber string `json:"pr_number"`
|
||||
PoNumber *string `json:"po_number"`
|
||||
PoDate *time.Time `json:"po_date"`
|
||||
CreditTerm int `json:"credit_term"`
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
type PurchaseListDTO struct {
|
||||
@@ -42,7 +43,6 @@ type PurchaseDetailDTO struct {
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||
}
|
||||
|
||||
|
||||
type PurchaseItemDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductID uint `json:"product_id"`
|
||||
@@ -59,16 +59,18 @@ type PurchaseItemDTO struct {
|
||||
TravelNumber *string `json:"travel_number"`
|
||||
TravelDocumentPath *string `json:"travel_document_path"`
|
||||
VehicleNumber *string `json:"vehicle_number"`
|
||||
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
|
||||
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
||||
return PurchaseRelationDTO{
|
||||
Id: p.Id,
|
||||
PrNumber: p.PrNumber,
|
||||
PoNumber: p.PoNumber,
|
||||
PoDate: p.PoDate,
|
||||
Notes: p.Notes,
|
||||
Id: p.Id,
|
||||
PrNumber: p.PrNumber,
|
||||
PoNumber: p.PoNumber,
|
||||
PoDate: p.PoDate,
|
||||
CreditTerm: p.CreditTerm,
|
||||
Notes: p.Notes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +109,20 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||
dto.Warehouse = &summary
|
||||
}
|
||||
|
||||
if item.ExpenseNonstock != nil {
|
||||
priceCopy := item.ExpenseNonstock.Price
|
||||
dto.TransportPerItem = &priceCopy
|
||||
|
||||
if item.ExpenseNonstock.Expense != nil {
|
||||
exp := item.ExpenseNonstock.Expense
|
||||
|
||||
if exp.Supplier != nil && exp.Supplier.Id != 0 {
|
||||
supplierSummary := supplierDTO.ToSupplierRelationDTO(*exp.Supplier)
|
||||
dto.ExpeditionVendor = &supplierSummary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
|
||||
@@ -189,9 +189,6 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
||||
if upd.VehicleNumber != nil {
|
||||
data["vehicle_number"] = upd.VehicleNumber
|
||||
}
|
||||
if upd.ReceivedQty != nil {
|
||||
data["total_qty"] = upd.ReceivedQty
|
||||
}
|
||||
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
|
||||
data["warehouse_id"] = upd.WarehouseID
|
||||
}
|
||||
|
||||
@@ -155,109 +155,6 @@ func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []enti
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
itemIDs := make([]uint, 0, len(updates))
|
||||
for _, upd := range updates {
|
||||
if upd.PurchaseItemID != 0 {
|
||||
itemIDs = append(itemIDs, upd.PurchaseItemID)
|
||||
}
|
||||
}
|
||||
if len(itemIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var links []struct {
|
||||
ItemID uint
|
||||
ExpenseNonstockID *uint64
|
||||
}
|
||||
if err := tx.Model(&entity.PurchaseItem{}).
|
||||
Select("id as item_id, expense_nonstock_id").
|
||||
Where("id IN ?", itemIDs).
|
||||
Scan(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expenseIDs := make(map[uint64]struct{})
|
||||
expenseNonstockIDs := make([]uint64, 0)
|
||||
for _, link := range links {
|
||||
if link.ExpenseNonstockID != nil && *link.ExpenseNonstockID != 0 {
|
||||
expenseNonstockIDs = append(expenseNonstockIDs, *link.ExpenseNonstockID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expenseNonstockIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, nsID := range expenseNonstockIDs {
|
||||
var expenseID uint64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Select("expense_id").
|
||||
Where("id = ?", nsID).
|
||||
Scan(&expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if expenseID != 0 {
|
||||
expenseIDs[expenseID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for expenseID := range expenseIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", expenseID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error {
|
||||
if len(expenseIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||
for _, id := range expenseIDs {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||
Where("expense_id = ?", id).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&entity.Expense{}, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
||||
if len(expenseIDs) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -99,6 +99,7 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
|
||||
return db.
|
||||
Preload("Supplier").
|
||||
Preload("CreatedUser").
|
||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("id ASC")
|
||||
}).
|
||||
@@ -109,7 +110,10 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Items.Product.Flags").
|
||||
Preload("Items.Warehouse.Area").
|
||||
Preload("Items.Warehouse.Location").
|
||||
Preload("Items.ProductWarehouse")
|
||||
Preload("Items.ProductWarehouse").
|
||||
Preload("Items.ExpenseNonstock").
|
||||
Preload("Items.ExpenseNonstock.Expense").
|
||||
Preload("Items.ExpenseNonstock.Expense.Supplier")
|
||||
}
|
||||
|
||||
func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) {
|
||||
@@ -121,7 +125,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
|
||||
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
||||
if err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
return nil, 0, utils.BadRequest(err.Error())
|
||||
}
|
||||
|
||||
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
@@ -180,7 +184,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get purchases: %+v", err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchases")
|
||||
return nil, 0, utils.Internal("Failed to get purchases")
|
||||
}
|
||||
|
||||
for i := range purchases {
|
||||
@@ -193,19 +197,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
||||
}
|
||||
|
||||
func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) {
|
||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get purchase: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
return purchase, nil
|
||||
return s.loadPurchase(c.Context(), id)
|
||||
}
|
||||
|
||||
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||
@@ -220,10 +212,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
|
||||
if _, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
||||
return nil, utils.NotFound("Supplier not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get supplier: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
||||
return nil, utils.Internal("Failed to get supplier")
|
||||
}
|
||||
|
||||
type aggregatedItem struct {
|
||||
@@ -234,7 +226,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
||||
return nil, utils.BadRequest("Items must not be empty")
|
||||
}
|
||||
|
||||
warehouseCache := make(map[uint]*entity.Warehouse)
|
||||
@@ -249,24 +241,27 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
||||
return nil, nil, utils.NotFound(fmt.Sprintf("Warehouse %d not found", id))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
return nil, nil, utils.Internal("Failed to get warehouse")
|
||||
}
|
||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
||||
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
||||
}
|
||||
var pfkID *uint
|
||||
if s.ProjectFlockKandangRepo != nil {
|
||||
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
||||
if pfk.ClosedAt != nil {
|
||||
return nil, nil, utils.BadRequest("Project sudah closing")
|
||||
}
|
||||
idCopy := uint(pfk.Id)
|
||||
pfkID = &idCopy
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d has no active project flock", id))
|
||||
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d has no active project flock", id))
|
||||
} else if err != nil {
|
||||
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||
return nil, nil, utils.Internal("Failed to validate project flock")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,10 +282,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
linked, err := s.ProductRepo.IsLinkedToSupplier(c.Context(), item.ProductID, req.SupplierID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", item.ProductID, req.SupplierID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
||||
return nil, utils.Internal("Failed to validate product for supplier")
|
||||
}
|
||||
if !linked {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
||||
}
|
||||
productSupplierCache[item.ProductID] = true
|
||||
}
|
||||
@@ -315,17 +310,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
}
|
||||
|
||||
var dueDate *time.Time
|
||||
if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" {
|
||||
parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate))
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid due_date, expected YYYY-MM-DD")
|
||||
}
|
||||
parsed = parsed.UTC()
|
||||
dueDate = &parsed
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
d := now.AddDate(0, 0, req.CreditTerm)
|
||||
dueDate = &d
|
||||
|
||||
purchase := &entity.Purchase{
|
||||
SupplierId: uint(req.SupplierID),
|
||||
CreditTerm: req.CreditTerm,
|
||||
DueDate: dueDate,
|
||||
Notes: req.Notes,
|
||||
CreatedBy: uint(actorID),
|
||||
@@ -373,13 +364,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
||||
})
|
||||
if transactionErr != nil {
|
||||
s.Log.Errorf("Failed to create purchase: %+v", transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase")
|
||||
return nil, utils.Internal("Failed to create purchase")
|
||||
}
|
||||
|
||||
created, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load created purchase: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
return nil, utils.Internal("Failed to load purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), created); err != nil {
|
||||
@@ -405,17 +396,15 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
purchase, err := s.loadPurchase(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
if action == entity.ApprovalActionApproved {
|
||||
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var latestStep uint16
|
||||
@@ -429,7 +418,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
|
||||
isInitialApproval := latestStep < uint16(utils.PurchaseStepStaffPurchase)
|
||||
if isInitialApproval && latestStep != uint16(utils.PurchaseStepPengajuan) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval")
|
||||
return nil, utils.BadRequest("Purchase is not ready for staff approval")
|
||||
}
|
||||
|
||||
hasReceivingData := false
|
||||
@@ -442,8 +431,8 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
|
||||
syncReceiving := !isInitialApproval && hasReceivingData
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty for staff approval")
|
||||
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
||||
return nil, utils.BadRequest("Items must not be empty for staff approval")
|
||||
}
|
||||
|
||||
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
||||
@@ -491,18 +480,18 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
||||
return nil, utils.NotFound("Purchase item not found")
|
||||
}
|
||||
if isInitialApproval {
|
||||
s.Log.Errorf("Failed to approve purchase %d: %+v", purchase.Id, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase")
|
||||
return nil, utils.Internal("Failed to approve purchase")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update purchase pricing")
|
||||
return nil, utils.Internal("Failed to update purchase pricing")
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
return nil, utils.Internal("Failed to load purchase")
|
||||
}
|
||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||
@@ -526,22 +515,19 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
||||
purchase, err := s.loadPurchase(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionApproved {
|
||||
if err := s.ensureProjectFlockNotClosedForPurchase(c.Context(), purchase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Log.Errorf("Failed to get purchase: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
|
||||
if purchase.LatestApproval == nil ||
|
||||
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepStaffPurchase) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval")
|
||||
return nil, utils.BadRequest("Purchase must reach staff purchase step before manager approval")
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionRejected {
|
||||
@@ -601,7 +587,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
||||
})
|
||||
if transactionErr != nil {
|
||||
s.Log.Errorf("Failed to approve manager purchase %d: %+v", purchase.Id, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate purchase order")
|
||||
return nil, utils.Internal("Failed to generate purchase order")
|
||||
}
|
||||
|
||||
if generatedNumber != "" {
|
||||
@@ -612,7 +598,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load purchase after manager approval: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
return nil, utils.Internal("Failed to load purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||
@@ -639,29 +625,26 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return nil, err
|
||||
}
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
purchase, err := s.loadPurchase(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase ")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if purchase.PoNumber == nil || strings.TrimSpace(*purchase.PoNumber) == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase order has not been generated")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
return nil, utils.BadRequest("Purchase order has not been generated")
|
||||
}
|
||||
|
||||
if purchase.LatestApproval == nil ||
|
||||
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepManager) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must be approved by manager before receiving products")
|
||||
return nil, utils.BadRequest("Purchase must be approved by manager before receiving products")
|
||||
}
|
||||
if action == entity.ApprovalActionApproved {
|
||||
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must not be empty")
|
||||
return nil, utils.BadRequest("Receiving data must not be empty")
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionRejected {
|
||||
@@ -670,7 +653,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
}
|
||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
return nil, utils.Internal("Failed to load purchase")
|
||||
}
|
||||
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
@@ -696,17 +679,22 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
|
||||
visitedItems := make(map[uint]struct{}, len(req.Items))
|
||||
prepared := make([]preparedReceiving, 0, len(req.Items))
|
||||
var earliestReceived *time.Time
|
||||
for _, payload := range req.Items {
|
||||
item, exists := itemMap[payload.PurchaseItemID]
|
||||
if !exists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||
}
|
||||
|
||||
receivedDate, err := utils.ParseDateString(payload.ReceivedDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
||||
}
|
||||
receivedDate = receivedDate.UTC()
|
||||
if earliestReceived == nil || receivedDate.Before(*earliestReceived) {
|
||||
copy := receivedDate
|
||||
earliestReceived = ©
|
||||
}
|
||||
|
||||
warehouseID := uint(item.WarehouseId)
|
||||
overrideWarehouse := false
|
||||
@@ -715,10 +703,10 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
overrideWarehouse = true
|
||||
}
|
||||
if warehouseID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
||||
}
|
||||
if payload.WarehouseID != nil && uint(item.WarehouseId) != warehouseID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving does not allow changing warehouse")
|
||||
return nil, utils.BadRequest("Receiving does not allow changing warehouse")
|
||||
}
|
||||
|
||||
var receivedQty float64
|
||||
@@ -728,14 +716,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
receivedQty = item.SubQty
|
||||
}
|
||||
if receivedQty < 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
||||
}
|
||||
if receivedQty > item.SubQty {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
||||
}
|
||||
|
||||
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
||||
}
|
||||
visitedItems[payload.PurchaseItemID] = struct{}{}
|
||||
|
||||
@@ -747,7 +735,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
var transportPerItem *float64
|
||||
if payload.TransportPerItem != nil {
|
||||
if *payload.TransportPerItem < 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
||||
}
|
||||
val := *payload.TransportPerItem
|
||||
transportPerItem = &val
|
||||
@@ -768,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
// Require receiving payload to cover all purchase items so that
|
||||
// receiving cannot be submitted partially item-by-item.
|
||||
if len(visitedItems) != len(itemMap) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must be provided for all purchase items")
|
||||
return nil, utils.BadRequest("Receiving data must be provided for all purchase items")
|
||||
}
|
||||
|
||||
receivingAction := action
|
||||
@@ -792,7 +780,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to inspect receiving approval for purchase %d: %+v", purchase.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
||||
return nil, utils.Internal("Failed to record purchase receiving")
|
||||
}
|
||||
|
||||
if latestReceiving != nil {
|
||||
@@ -826,7 +814,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
|
||||
// Always ensure PW when qty > 0 so stockable has target.
|
||||
if prep.receivedQty > 0 {
|
||||
pwID, err := pwRepoTx.EnsureProductWarehouse(c.Context(), uint(item.ProductId), prep.warehouseID, purchase.CreatedBy)
|
||||
pwID, err := pwRepoTx.EnsureProductWarehouse(
|
||||
c.Context(),
|
||||
uint(item.ProductId),
|
||||
prep.warehouseID,
|
||||
item.ProjectFlockKandangId,
|
||||
purchase.CreatedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -882,6 +876,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return err
|
||||
}
|
||||
|
||||
// Update due_date based on earliest received date when receiving approved.
|
||||
if earliestReceived != nil {
|
||||
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
|
||||
if err := tx.Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchase.Id).
|
||||
Update("due_date", due).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.FifoSvc != nil {
|
||||
for _, adj := range fifoAdds {
|
||||
if adj.pwID == 0 || adj.qty <= 0 {
|
||||
@@ -903,15 +907,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found for receiving")
|
||||
return nil, utils.NotFound("Purchase item not found for receiving")
|
||||
}
|
||||
s.Log.Errorf("Failed to save purchase receiving %d: %+v", purchase.Id, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
||||
return nil, utils.Internal("Failed to record purchase receiving")
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase ")
|
||||
return nil, utils.Internal("Failed to load purchase ")
|
||||
}
|
||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||
@@ -936,7 +940,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return nil, fe
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
return nil, utils.Internal("Failed to sync expense")
|
||||
}
|
||||
|
||||
// Create approvals only after expense sync succeeds
|
||||
@@ -959,20 +963,23 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
return nil, utils.NotFound("Purchase not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
return nil, utils.Internal("Failed to get purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if purchase.LatestApproval == nil || purchase.LatestApproval.StepNumber == uint16(utils.PurchaseStepPengajuan) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase cannot delete items before staff purchase approval")
|
||||
return nil, utils.BadRequest("Purchase cannot delete items before staff purchase approval")
|
||||
}
|
||||
|
||||
if len(purchase.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to delete")
|
||||
return nil, utils.BadRequest("Purchase has no items to delete")
|
||||
}
|
||||
|
||||
requested := make(map[uint]struct{}, len(req.ItemIDs))
|
||||
@@ -992,7 +999,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
}
|
||||
|
||||
if len(toDelete) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Requested items were not found in this purchase")
|
||||
return nil, utils.BadRequest("Requested items were not found in this purchase")
|
||||
}
|
||||
|
||||
toDeleteSet := make(map[uint]struct{}, len(toDelete))
|
||||
@@ -1007,7 +1014,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
}
|
||||
|
||||
if len(purchase.Items)-len(toDelete) <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must keep at least one item")
|
||||
return nil, utils.BadRequest("Purchase must keep at least one item")
|
||||
}
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
@@ -1021,9 +1028,9 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
||||
return nil, utils.NotFound("Purchase item not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase items")
|
||||
return nil, utils.Internal("Failed to delete purchase items")
|
||||
}
|
||||
|
||||
if len(itemsToDelete) > 0 {
|
||||
@@ -1032,13 +1039,13 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return nil, fe
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
return nil, utils.Internal("Failed to sync expense")
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
return nil, utils.Internal("Failed to load purchase")
|
||||
}
|
||||
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||
@@ -1049,16 +1056,16 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
|
||||
func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
if id == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||
return utils.BadRequest("Invalid purchase id")
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
purchase, err := s.loadPurchase(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
||||
return err
|
||||
}
|
||||
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
|
||||
@@ -1080,9 +1087,9 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
||||
return utils.NotFound("Purchase not found")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase")
|
||||
return utils.Internal("Failed to delete purchase")
|
||||
}
|
||||
|
||||
if len(itemsToDelete) > 0 {
|
||||
@@ -1091,7 +1098,7 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
if fe, ok := err.(*fiber.Error); ok {
|
||||
return fe
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
||||
return utils.Internal("Failed to sync expense")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,7 +1116,7 @@ func (s *purchaseService) createPurchaseApproval(
|
||||
allowDuplicate bool,
|
||||
) error {
|
||||
if purchaseID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval")
|
||||
return utils.BadRequest("Purchase is invalid for approval")
|
||||
}
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
@@ -1117,7 +1124,7 @@ func (s *purchaseService) createPurchaseApproval(
|
||||
|
||||
svc := s.approvalServiceForDB(db)
|
||||
if svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Approval service not available")
|
||||
return utils.Internal("Approval service not available")
|
||||
}
|
||||
|
||||
modifier := func(db *gorm.DB) *gorm.DB {
|
||||
@@ -1175,7 +1182,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
syncReceiving bool,
|
||||
) (*staffAdjustmentPayload, error) {
|
||||
if len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
||||
return nil, utils.BadRequest("Items must not be empty")
|
||||
}
|
||||
|
||||
requestItems := make(map[uint]validation.StaffPurchaseApprovalItem, len(req.Items))
|
||||
@@ -1187,7 +1194,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
continue
|
||||
}
|
||||
if _, exists := requestItems[item.PurchaseItemID]; exists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
||||
}
|
||||
requestItems[item.PurchaseItemID] = item
|
||||
}
|
||||
@@ -1205,34 +1212,31 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
allowedWarehouses[item.WarehouseId] = struct{}{}
|
||||
}
|
||||
if len(allowedWarehouses) == 0 && len(newPayloads) > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
|
||||
return nil, utils.BadRequest("No available warehouses for this purchase")
|
||||
}
|
||||
|
||||
for _, item := range purchase.Items {
|
||||
data, ok := requestItems[item.Id]
|
||||
if !ok {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||
}
|
||||
if data.ProductID != 0 && data.ProductID != item.ProductId {
|
||||
return nil, fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id),
|
||||
)
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id))
|
||||
}
|
||||
if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
||||
}
|
||||
|
||||
effectiveQty := item.SubQty
|
||||
if data.Qty != nil {
|
||||
if *data.Qty <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
||||
}
|
||||
if item.TotalUsed > 0 && *data.Qty < item.TotalUsed {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
||||
}
|
||||
if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
||||
}
|
||||
effectiveQty = *data.Qty
|
||||
}
|
||||
@@ -1261,7 +1265,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
delete(requestItems, item.Id)
|
||||
}
|
||||
if len(requestItems) > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
|
||||
return nil, utils.BadRequest("Found pricing data for items that do not belong to this purchase")
|
||||
}
|
||||
|
||||
productSupplierCache := make(map[uint]bool)
|
||||
@@ -1270,37 +1274,28 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
|
||||
for _, payload := range newPayloads {
|
||||
if payload.ProductID == 0 || payload.WarehouseID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Product and warehouse must be provided for new items")
|
||||
return nil, utils.BadRequest("Product and warehouse must be provided for new items")
|
||||
}
|
||||
if payload.Qty == nil || *payload.Qty <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
||||
}
|
||||
if _, ok := allowedWarehouses[payload.WarehouseID]; !ok {
|
||||
return nil, fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID),
|
||||
)
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID))
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d:%d", payload.ProductID, payload.WarehouseID)
|
||||
if _, exists := existingCombos[key]; exists {
|
||||
return nil, fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID),
|
||||
)
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID))
|
||||
}
|
||||
|
||||
if _, checked := productSupplierCache[payload.ProductID]; !checked {
|
||||
linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(payload.ProductID), uint(purchase.SupplierId))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", payload.ProductID, purchase.SupplierId, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
||||
return nil, utils.Internal("Failed to validate product for supplier")
|
||||
}
|
||||
if !linked {
|
||||
return nil, fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId),
|
||||
)
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId))
|
||||
}
|
||||
productSupplierCache[payload.ProductID] = true
|
||||
}
|
||||
@@ -1328,7 +1323,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
}
|
||||
|
||||
if len(updates) == 0 && len(newItems) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process")
|
||||
return nil, utils.BadRequest("Purchase has no items to process")
|
||||
}
|
||||
|
||||
return &staffAdjustmentPayload{
|
||||
@@ -1340,10 +1335,10 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
||||
// ? helper
|
||||
func calculateTotalPrice(quantity float64, price float64, provided *float64, ref string) (float64, error) {
|
||||
if quantity <= 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
||||
return 0, utils.BadRequest(fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
||||
}
|
||||
if price <= 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for %s must be greater than 0", ref))
|
||||
return 0, utils.BadRequest(fmt.Sprintf("Price for %s must be greater than 0", ref))
|
||||
}
|
||||
|
||||
expectedTotal := price * quantity
|
||||
@@ -1352,10 +1347,10 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref
|
||||
return expectedTotal, nil
|
||||
}
|
||||
if *provided <= 0 {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
||||
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
||||
}
|
||||
if math.Abs(*provided-expectedTotal) > priceTolerance {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
||||
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
||||
}
|
||||
return *provided, nil
|
||||
}
|
||||
@@ -1384,7 +1379,7 @@ func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
||||
case string(entity.ApprovalActionRejected):
|
||||
return entity.ApprovalActionRejected, nil
|
||||
default:
|
||||
return "", fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||
return "", utils.BadRequest("action must be APPROVED or REJECTED")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1399,13 +1394,58 @@ func (s *purchaseService) rejectAndReload(
|
||||
if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchaseID, s.withRelations)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
||||
}
|
||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||
}
|
||||
return updated, nil
|
||||
return s.loadPurchase(c.Context(), purchaseID)
|
||||
}
|
||||
func (s *purchaseService) loadPurchase(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
) (*entity.Purchase, error) {
|
||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, utils.NotFound("Purchase not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get purchase %d: %+v", id, err)
|
||||
return nil, utils.Internal("Failed to get purchase")
|
||||
}
|
||||
|
||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
|
||||
return purchase, nil
|
||||
}
|
||||
|
||||
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
||||
seen := make(map[uint]struct{})
|
||||
ids := make([]uint, 0)
|
||||
|
||||
for _, item := range p.Items {
|
||||
if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 {
|
||||
continue
|
||||
}
|
||||
id := uint(*item.ProjectFlockKandangId)
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
|
||||
ctx context.Context,
|
||||
purchase *entity.Purchase,
|
||||
) error {
|
||||
pfkIDs := collectPFKIDsFromPurchase(purchase)
|
||||
if len(pfkIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.PurchaseRepo.DB()
|
||||
if db == nil {
|
||||
return utils.Internal("DB not available for project flock validation")
|
||||
}
|
||||
|
||||
return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type PurchaseItemPayload struct {
|
||||
|
||||
type CreatePurchaseRequest struct {
|
||||
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||
CreditTerm int `json:"credit_term" validate:"required,number,gte=0"`
|
||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type RepportController struct {
|
||||
RepportService service.RepportService
|
||||
}
|
||||
|
||||
func NewRepportController(repportService service.RepportService) *RepportController {
|
||||
return &RepportController{
|
||||
RepportService: repportService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
|
||||
query := &validation.ExpenseQuery{
|
||||
Page: ctx.QueryInt("page", 1),
|
||||
Limit: ctx.QueryInt("limit", 10),
|
||||
Search: ctx.Query("search", ""),
|
||||
Category: ctx.Query("category", ""),
|
||||
SupplierId: int64(ctx.QueryInt("supplier_id", 0)),
|
||||
KandangId: int64(ctx.QueryInt("kandang_id", 0)),
|
||||
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)),
|
||||
NonstockId: int64(ctx.QueryInt("nonstock_id", 0)),
|
||||
AreaId: int64(ctx.QueryInt("area_id", 0)),
|
||||
LocationId: int64(ctx.QueryInt("location_id", 0)),
|
||||
RealizationDate: ctx.Query("realization_date", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := c.RepportService.GetExpense(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.RepportExpenseListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get expense report successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
||||
query := &validation.MarketingQuery{
|
||||
Page: ctx.QueryInt("page", 1),
|
||||
Limit: ctx.QueryInt("limit", 10),
|
||||
Search: ctx.Query("search", ""),
|
||||
CustomerId: int64(ctx.QueryInt("customer_id", 0)),
|
||||
ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)),
|
||||
DeliveryDate: ctx.Query("delivery_date", ""),
|
||||
ProductId: int64(ctx.QueryInt("product_id", 0)),
|
||||
WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)),
|
||||
SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)),
|
||||
MarketingId: int64(ctx.QueryInt("marketing_id", 0)),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := c.RepportService.GetMarketing(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get marketing report successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type RepportExpenseBaseDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
PoNumber string `json:"po_number"`
|
||||
Category string `json:"category"`
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
||||
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RepportExpensePengajuanDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
ExpenseId *uint64 `json:"expense_id,omitempty"`
|
||||
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
Price float64 `json:"price"`
|
||||
Notes string `json:"notes"`
|
||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RepportExpenseRealisasiDTO struct {
|
||||
Id *uint64 `json:"id,omitempty"`
|
||||
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
Price float64 `json:"price"`
|
||||
Notes string `json:"notes"`
|
||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RepportExpenseListDTO struct {
|
||||
RepportExpenseBaseDTO
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
Pengajuan RepportExpensePengajuanDTO `json:"pengajuan"`
|
||||
Realisasi RepportExpenseRealisasiDTO `json:"realisasi"`
|
||||
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||
TotalRealisasi float64 `json:"total_realisasi"`
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
|
||||
}
|
||||
|
||||
// === MAPPERS ===
|
||||
|
||||
func ToRepportExpenseBaseDTO(e *entity.Expense) RepportExpenseBaseDTO {
|
||||
var realizationDate *time.Time
|
||||
if !e.RealizationDate.IsZero() {
|
||||
realizationDate = &e.RealizationDate
|
||||
}
|
||||
|
||||
var supplier *supplierDTO.SupplierRelationDTO
|
||||
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||
mapped := supplierDTO.ToSupplierRelationDTO(*e.Supplier)
|
||||
supplier = &mapped
|
||||
}
|
||||
|
||||
return RepportExpenseBaseDTO{
|
||||
Id: e.Id,
|
||||
ReferenceNumber: e.ReferenceNumber,
|
||||
PoNumber: e.PoNumber,
|
||||
Category: e.Category,
|
||||
Supplier: supplier,
|
||||
RealizationDate: realizationDate,
|
||||
TransactionDate: e.TransactionDate,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportExpensePengajuanDTO(ns *entity.ExpenseNonstock) RepportExpensePengajuanDTO {
|
||||
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
||||
nonstock = &mapped
|
||||
}
|
||||
|
||||
return RepportExpensePengajuanDTO{
|
||||
Id: ns.Id,
|
||||
ExpenseId: ns.ExpenseId,
|
||||
ProjectFlockKandangId: ns.ProjectFlockKandangId,
|
||||
Qty: ns.Qty,
|
||||
Price: ns.Price,
|
||||
Notes: ns.Notes,
|
||||
Nonstock: nonstock,
|
||||
CreatedAt: ns.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportExpenseRealisasiDTO(r *entity.ExpenseRealization) RepportExpenseRealisasiDTO {
|
||||
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||
if r.ExpenseNonstock != nil && r.ExpenseNonstock.Nonstock != nil && r.ExpenseNonstock.Nonstock.Id != 0 {
|
||||
mapped := nonstockDTO.ToNonstockRelationDTO(*r.ExpenseNonstock.Nonstock)
|
||||
nonstock = &mapped
|
||||
}
|
||||
|
||||
return RepportExpenseRealisasiDTO{
|
||||
Id: r.ExpenseNonstockId,
|
||||
ExpenseNonstockId: r.ExpenseNonstockId,
|
||||
Qty: r.Qty,
|
||||
Price: r.Price,
|
||||
Notes: r.Notes,
|
||||
Nonstock: nonstock,
|
||||
CreatedAt: r.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportExpenseListDTO(baseDTO RepportExpenseBaseDTO, ns *entity.ExpenseNonstock, latestApproval *approvalDTO.ApprovalRelationDTO) RepportExpenseListDTO {
|
||||
var realisasi RepportExpenseRealisasiDTO
|
||||
if ns.Realization != nil {
|
||||
realisasi = ToRepportExpenseRealisasiDTO(ns.Realization)
|
||||
}
|
||||
|
||||
totalPengajuan := ns.Qty * ns.Price
|
||||
totalRealisasi := float64(0)
|
||||
if ns.Realization != nil {
|
||||
totalRealisasi = ns.Realization.Qty * ns.Realization.Price
|
||||
}
|
||||
|
||||
// Get kandang data at the main level
|
||||
var kandang *kandangDTO.KandangRelationDTO
|
||||
if ns.Kandang != nil && ns.Kandang.Id != 0 {
|
||||
mapped := kandangDTO.ToKandangRelationDTO(*ns.Kandang)
|
||||
kandang = &mapped
|
||||
}
|
||||
|
||||
return RepportExpenseListDTO{
|
||||
RepportExpenseBaseDTO: baseDTO,
|
||||
Kandang: kandang,
|
||||
Pengajuan: ToRepportExpensePengajuanDTO(ns),
|
||||
Realisasi: realisasi,
|
||||
TotalPengajuan: totalPengajuan,
|
||||
TotalRealisasi: totalRealisasi,
|
||||
LatestApproval: latestApproval,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportExpenseListDTOs(realizations []entity.ExpenseRealization) []RepportExpenseListDTO {
|
||||
result := make([]RepportExpenseListDTO, 0, len(realizations))
|
||||
|
||||
for _, realization := range realizations {
|
||||
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Expense == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
expense := realization.ExpenseNonstock.Expense
|
||||
baseDTO := ToRepportExpenseBaseDTO(expense)
|
||||
|
||||
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||
if expense.LatestApproval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*expense.LatestApproval)
|
||||
latestApproval = &mapped
|
||||
}
|
||||
|
||||
// Create a temporary realization with the current realization data
|
||||
if realization.ExpenseNonstock.Realization == nil {
|
||||
realization.ExpenseNonstock.Realization = &realization
|
||||
}
|
||||
|
||||
dto := ToRepportExpenseListDTO(baseDTO, realization.ExpenseNonstock, latestApproval)
|
||||
result = append(result, dto)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type RepportMarketingBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
SoNumber string `json:"so_number"`
|
||||
SoDate time.Time `json:"so_date"`
|
||||
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||
SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RepportMarketingProductDTO struct {
|
||||
Id uint `json:"id"`
|
||||
MarketingProductId uint `json:"marketing_product_id"`
|
||||
Qty float64 `json:"qty"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
AvgWeight float64 `json:"avg_weight"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RepportMarketingDeliveryDTO struct {
|
||||
Id uint `json:"id"`
|
||||
MarketingProductId uint `json:"marketing_product_id"`
|
||||
Qty float64 `json:"qty"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
AvgWeight float64 `json:"avg_weight"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
DeliveryDate *time.Time `json:"delivery_date,omitempty"`
|
||||
VehicleNumber string `json:"vehicle_number"`
|
||||
DoNumber string `json:"do_number"`
|
||||
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RepportMarketingListDTO struct {
|
||||
RepportMarketingBaseDTO
|
||||
MarketingProduct RepportMarketingProductDTO `json:"marketing_product"`
|
||||
MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"`
|
||||
TotalMarketingProduct float64 `json:"total_marketing_product"`
|
||||
TotalMarketingDelivery float64 `json:"total_marketing_delivery"`
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
|
||||
}
|
||||
|
||||
// === MAPPERS ===
|
||||
|
||||
func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO {
|
||||
if m == nil {
|
||||
return RepportMarketingBaseDTO{}
|
||||
}
|
||||
|
||||
var customer *customerDTO.CustomerRelationDTO
|
||||
if m.Customer.Id != 0 {
|
||||
mapped := customerDTO.ToCustomerRelationDTO(m.Customer)
|
||||
customer = &mapped
|
||||
}
|
||||
|
||||
var salesPerson *userDTO.UserRelationDTO
|
||||
if m.SalesPerson.Id != 0 {
|
||||
mapped := userDTO.ToUserRelationDTO(m.SalesPerson)
|
||||
salesPerson = &mapped
|
||||
}
|
||||
|
||||
return RepportMarketingBaseDTO{
|
||||
Id: m.Id,
|
||||
SoNumber: m.SoNumber,
|
||||
SoDate: m.SoDate,
|
||||
Customer: customer,
|
||||
SalesPerson: salesPerson,
|
||||
Notes: m.Notes,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO {
|
||||
if mp == nil {
|
||||
return RepportMarketingProductDTO{}
|
||||
}
|
||||
|
||||
var product *productDTO.ProductRelationDTO
|
||||
if mp.ProductWarehouse.Product.Id != 0 {
|
||||
mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product)
|
||||
product = &mapped
|
||||
}
|
||||
|
||||
return RepportMarketingProductDTO{
|
||||
Id: mp.Id,
|
||||
MarketingProductId: mp.Id,
|
||||
Qty: mp.Qty,
|
||||
UnitPrice: mp.UnitPrice,
|
||||
AvgWeight: mp.AvgWeight,
|
||||
TotalWeight: mp.TotalWeight,
|
||||
TotalPrice: mp.TotalPrice,
|
||||
Product: product,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO {
|
||||
if mdp == nil {
|
||||
return RepportMarketingDeliveryDTO{}
|
||||
}
|
||||
|
||||
var product *productDTO.ProductRelationDTO
|
||||
if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||
mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product)
|
||||
product = &mapped
|
||||
}
|
||||
|
||||
warehouseId := uint(0)
|
||||
if mdp.MarketingProduct.ProductWarehouse.Id != 0 {
|
||||
warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId
|
||||
}
|
||||
|
||||
doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId)
|
||||
|
||||
return RepportMarketingDeliveryDTO{
|
||||
Id: mdp.Id,
|
||||
MarketingProductId: mdp.MarketingProductId,
|
||||
Qty: mdp.Qty,
|
||||
UnitPrice: mdp.UnitPrice,
|
||||
TotalWeight: mdp.TotalWeight,
|
||||
AvgWeight: mdp.AvgWeight,
|
||||
TotalPrice: mdp.TotalPrice,
|
||||
DeliveryDate: mdp.DeliveryDate,
|
||||
VehicleNumber: mdp.VehicleNumber,
|
||||
DoNumber: doNumber,
|
||||
Product: product,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO {
|
||||
var marketingProduct RepportMarketingProductDTO
|
||||
var marketingDelivery RepportMarketingDeliveryDTO
|
||||
|
||||
if mp != nil {
|
||||
marketingProduct = ToRepportMarketingProductDTO(mp)
|
||||
}
|
||||
|
||||
if mdp != nil {
|
||||
marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber)
|
||||
}
|
||||
|
||||
totalMarketingProduct := float64(0)
|
||||
totalMarketingDelivery := float64(0)
|
||||
|
||||
if mp != nil {
|
||||
totalMarketingProduct = mp.Qty * mp.UnitPrice
|
||||
}
|
||||
|
||||
if mdp != nil {
|
||||
totalMarketingDelivery = mdp.Qty * mdp.UnitPrice
|
||||
}
|
||||
|
||||
return RepportMarketingListDTO{
|
||||
RepportMarketingBaseDTO: baseDTO,
|
||||
MarketingProduct: marketingProduct,
|
||||
MarketingDelivery: marketingDelivery,
|
||||
TotalMarketingProduct: totalMarketingProduct,
|
||||
TotalMarketingDelivery: totalMarketingDelivery,
|
||||
LatestApproval: latestApproval,
|
||||
}
|
||||
}
|
||||
|
||||
func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO {
|
||||
result := make([]RepportMarketingListDTO, 0, len(deliveryProducts))
|
||||
|
||||
marketingMap := make(map[uint]entity.MarketingDeliveryProduct)
|
||||
for _, dp := range deliveryProducts {
|
||||
if dp.MarketingProduct.Marketing.Id == 0 {
|
||||
continue
|
||||
}
|
||||
marketingID := dp.MarketingProduct.Marketing.Id
|
||||
if _, exists := marketingMap[marketingID]; !exists {
|
||||
marketingMap[marketingID] = dp
|
||||
}
|
||||
}
|
||||
|
||||
for _, deliveryProduct := range marketingMap {
|
||||
if deliveryProduct.MarketingProduct.Marketing.Id == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
marketing := &deliveryProduct.MarketingProduct.Marketing
|
||||
baseDTO := ToRepportMarketingBaseDTO(marketing)
|
||||
|
||||
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||
if marketing.LatestApproval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
|
||||
latestApproval = &mapped
|
||||
}
|
||||
|
||||
mdp := &deliveryProduct
|
||||
dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval)
|
||||
result = append(result, dto)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package repports
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
)
|
||||
|
||||
type RepportModule struct{}
|
||||
|
||||
func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
|
||||
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
|
||||
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
|
||||
approvalRepository := commonRepo.NewApprovalRepository(db)
|
||||
|
||||
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
||||
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc)
|
||||
|
||||
RepportRoutes(router, repportService)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package repports
|
||||
|
||||
import (
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers"
|
||||
repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
|
||||
ctrl := controller.NewRepportController(s)
|
||||
|
||||
route := v1.Group("/reports")
|
||||
|
||||
route.Get("/expense", ctrl.GetExpense)
|
||||
route.Get("/marketing", ctrl.GetMarketing)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RepportService interface {
|
||||
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
|
||||
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error)
|
||||
}
|
||||
|
||||
type repportService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
||||
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
|
||||
ApprovalSvc approvalService.ApprovalService
|
||||
}
|
||||
|
||||
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService {
|
||||
return &repportService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||
MarketingDeliveryRepo: marketingDeliveryRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||
if err != nil {
|
||||
s.Log.Errorf("GetAllWithFilters error: %v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
result := dto.ToRepportExpenseListDTOs(realizations)
|
||||
|
||||
expenseIDs := make([]uint, 0, len(result))
|
||||
for i := range result {
|
||||
expenseIDs = append(expenseIDs, uint(result[i].Id))
|
||||
}
|
||||
|
||||
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("LatestByTargets error: %v", err)
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
expenseIDAsUint := uint(result[i].Id)
|
||||
if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*approval)
|
||||
result[i].LatestApproval = &mapped
|
||||
}
|
||||
}
|
||||
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
marketingIDMap := make(map[uint]bool)
|
||||
marketingIDs := make([]uint, 0)
|
||||
for _, dp := range deliveryProducts {
|
||||
if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] {
|
||||
marketingIDs = append(marketingIDs, marketingID)
|
||||
marketingIDMap[marketingID] = true
|
||||
}
|
||||
}
|
||||
|
||||
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("LatestByTargets error: %v", err)
|
||||
}
|
||||
|
||||
for i := range deliveryProducts {
|
||||
if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil {
|
||||
deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval
|
||||
}
|
||||
}
|
||||
|
||||
return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package validation
|
||||
|
||||
type ExpenseQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||
SupplierId int64 `query:"supplier_id" validate:"omitempty"`
|
||||
KandangId int64 `query:"kandang_id" validate:"omitempty"`
|
||||
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"`
|
||||
ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"`
|
||||
NonstockId int64 `query:"nonstock_id" validate:"omitempty"`
|
||||
AreaId int64 `query:"area_id" validate:"omitempty"`
|
||||
LocationId int64 `query:"location_id" validate:"omitempty"`
|
||||
RealizationDate string `query:"realization_date" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type MarketingQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
CustomerId int64 `query:"customer_id" validate:"omitempty"`
|
||||
ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"`
|
||||
DeliveryDate string `query:"delivery_date" validate:"omitempty"`
|
||||
ProductId int64 `query:"product_id" validate:"omitempty"`
|
||||
WarehouseId int64 `query:"warehouse_id" validate:"omitempty"`
|
||||
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
|
||||
MarketingId int64 `query:"marketing_id" validate:"omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user