Merge branch 'codex/filter-improvement' into 'development'

Codex/po date

See merge request mbugroup/lti-api!479
This commit is contained in:
Adnan Zahir
2026-04-25 22:50:20 +07:00
6 changed files with 100 additions and 15 deletions
@@ -283,6 +283,32 @@ func validatePurchaseDocumentSizes(files []*multipart.FileHeader) error {
return nil return nil
} }
func (ctrl *PurchaseController) UpdatePoDate(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
}
req := new(validation.UpdatePoDateRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.UpdatePoDate(c, uint(id), req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase PO date updated successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error { func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.Atoi(param)
@@ -78,12 +78,13 @@ func setPurchaseExportColumns(file *excelize.File, sheet string) error {
"A": 16, "A": 16,
"B": 16, "B": 16,
"C": 14, "C": 14,
"D": 22, "D": 14,
"E": 22, "E": 22,
"F": 18, "F": 22,
"G": 18, "G": 18,
"H": 52, "H": 18,
"I": 24, "I": 52,
"J": 24,
} }
for col, width := range columnWidths { for col, width := range columnWidths {
@@ -103,6 +104,7 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
"PR Number", "PR Number",
"PO Number", "PO Number",
"Tanggal PO", "Tanggal PO",
"Tanggal Terima",
"Supplier", "Supplier",
"Lokasi", "Lokasi",
"Status", "Status",
@@ -136,7 +138,7 @@ func setPurchaseExportHeaders(file *excelize.File, sheet string) error {
return err return err
} }
return file.SetCellStyle(sheet, "A1", "I1", headerStyle) return file.SetCellStyle(sheet, "A1", "J1", headerStyle)
} }
func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.PurchaseListDTO, grandTotals map[uint]float64) error { func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.PurchaseListDTO, grandTotals map[uint]float64) error {
@@ -155,22 +157,25 @@ func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.Purcha
if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(item.PoDate)); err != nil { if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(item.PoDate)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "D"+row, safePurchaseSupplierName(item)); err != nil { if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "E"+row, safePurchaseLocationName(item)); err != nil { if err := file.SetCellValue(sheet, "E"+row, safePurchaseSupplierName(item)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "F"+row, formatPurchaseExportStatus(item)); err != nil { if err := file.SetCellValue(sheet, "F"+row, safePurchaseLocationName(item)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "G"+row, formatPurchaseRupiah(grandTotals[item.Id])); err != nil { if err := file.SetCellValue(sheet, "G"+row, formatPurchaseExportStatus(item)); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "H"+row, formatPurchaseProducts(item)); err != nil { if err := file.SetCellValue(sheet, "H"+row, formatPurchaseRupiah(grandTotals[item.Id])); err != nil {
return err return err
} }
if err := file.SetCellValue(sheet, "I"+row, safePurchaseExportPointerText(item.Notes)); err != nil { if err := file.SetCellValue(sheet, "I"+row, formatPurchaseProducts(item)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+row, safePurchaseExportPointerText(item.Notes)); err != nil {
return err return err
} }
} }
@@ -192,7 +197,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.Purcha
if err != nil { if err != nil {
return err return err
} }
if err := file.SetCellStyle(sheet, "A2", "I"+strconv.Itoa(lastRow), dataStyle); err != nil { if err := file.SetCellStyle(sheet, "A2", "J"+strconv.Itoa(lastRow), dataStyle); err != nil {
return err return err
} }
@@ -212,7 +217,7 @@ func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.Purcha
return err return err
} }
return file.SetCellStyle(sheet, "G2", "G"+strconv.Itoa(lastRow), moneyStyle) return file.SetCellStyle(sheet, "H2", "H"+strconv.Itoa(lastRow), moneyStyle)
} }
func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 { func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 {
@@ -27,6 +27,7 @@ type PurchaseListDTO struct {
PurchaseRelationDTO PurchaseRelationDTO
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
DueDate *time.Time `json:"due_date"` DueDate *time.Time `json:"due_date"`
ReceivedDate *time.Time `json:"received_date"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
RequesterName string `json:"requester_name"` RequesterName string `json:"requester_name"`
PoExpedition []PoExpeditionDTO `json:"po_expedition"` PoExpedition []PoExpeditionDTO `json:"po_expedition"`
@@ -174,6 +175,7 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
poExpedition = make([]PoExpeditionDTO, 0) poExpedition = make([]PoExpeditionDTO, 0)
location *locationDTO.LocationRelationDTO location *locationDTO.LocationRelationDTO
area *areaDTO.AreaRelationDTO area *areaDTO.AreaRelationDTO
receivedDate *time.Time
) )
productMap := make(map[uint]productDTO.ProductRelationDTO) productMap := make(map[uint]productDTO.ProductRelationDTO)
expeditionRefSet := make(map[uint64]struct{}) expeditionRefSet := make(map[uint64]struct{})
@@ -205,6 +207,12 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
ar := areaDTO.ToAreaRelationDTO(item.Warehouse.Area) ar := areaDTO.ToAreaRelationDTO(item.Warehouse.Area)
area = &ar area = &ar
} }
if item.ReceivedDate != nil && !item.ReceivedDate.IsZero() {
if receivedDate == nil || item.ReceivedDate.Before(*receivedDate) {
t := *item.ReceivedDate
receivedDate = &t
}
}
} }
products := make([]productDTO.ProductRelationDTO, 0, len(productMap)) products := make([]productDTO.ProductRelationDTO, 0, len(productMap))
for _, prod := range productMap { for _, prod := range productMap {
@@ -215,6 +223,7 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
PurchaseRelationDTO: ToPurchaseRelationDTO(&p), PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier, Supplier: supplier,
DueDate: p.DueDate, DueDate: p.DueDate,
ReceivedDate: receivedDate,
CreatedUser: createdUser, CreatedUser: createdUser,
RequesterName: requesterName, RequesterName: requesterName,
PoExpedition: poExpedition, PoExpedition: poExpedition,
+1
View File
@@ -21,6 +21,7 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe
route.Post("/:id/approvals/staff", m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase) route.Post("/:id/approvals/staff", m.RequirePermissions(m.P_PurchaseApprovalStaff), ctrl.ApproveStaffPurchase)
route.Post("/:id/approvals/manager", m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase) route.Post("/:id/approvals/manager", m.RequirePermissions(m.P_PurchaseApprovalManager), ctrl.ApproveManagerPurchase)
route.Post("/:id/receipts", m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts) route.Post("/:id/receipts", m.RequirePermissions(m.P_PurchaseReceive), ctrl.ReceiveProducts)
route.Patch("/:id/po-date", m.RequirePermissions(m.P_PurchaseUpdateOne), ctrl.UpdatePoDate)
route.Delete("/:id", m.RequirePermissions(m.P_PurchaseDeleteOne), ctrl.DeletePurchase) route.Delete("/:id", m.RequirePermissions(m.P_PurchaseDeleteOne), ctrl.DeletePurchase)
route.Delete("/:id/items", m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems) route.Delete("/:id/items", m.RequirePermissions(m.P_PurchaseItemDeleteOne), ctrl.DeleteItems)
} }
@@ -45,6 +45,7 @@ type PurchaseService interface {
DeleteItems(ctx *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) DeleteItems(ctx *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error)
DeletePurchase(ctx *fiber.Ctx, id uint) error DeletePurchase(ctx *fiber.Ctx, id uint) error
GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error) GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
UpdatePoDate(ctx *fiber.Ctx, id uint, req *validation.UpdatePoDateRequest) (*entity.Purchase, error)
} }
const ( const (
@@ -713,6 +714,12 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
} }
now := time.Now().UTC() now := time.Now().UTC()
poDateToSet := now
if req.PoDate != nil && strings.TrimSpace(*req.PoDate) != "" {
if parsed, parseErr := utils.ParseDateString(strings.TrimSpace(*req.PoDate)); parseErr == nil {
poDateToSet = parsed.UTC()
}
}
hasExistingPO := purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" hasExistingPO := purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != ""
var generatedNumber string var generatedNumber string
@@ -725,7 +732,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
return err return err
} }
updateData["po_number"] = code updateData["po_number"] = code
updateData["po_date"] = now updateData["po_date"] = poDateToSet
generatedNumber = code generatedNumber = code
} }
@@ -770,7 +777,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
if generatedNumber != "" { if generatedNumber != "" {
purchase.PoNumber = &generatedNumber purchase.PoNumber = &generatedNumber
purchase.PoDate = &now purchase.PoDate = &poDateToSet
} }
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations) updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
@@ -792,6 +799,38 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
return updated, nil return updated, nil
} }
func (s *purchaseService) UpdatePoDate(c *fiber.Ctx, id uint, req *validation.UpdatePoDateRequest) (*entity.Purchase, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
parsed, err := utils.ParseDateString(strings.TrimSpace(req.PoDate))
if err != nil {
return nil, utils.BadRequest("po_date must use format YYYY-MM-DD")
}
poDate := parsed.UTC()
purchase, err := s.loadPurchase(c.Context(), id)
if err != nil {
return nil, err
}
if err := s.PurchaseRepo.PatchOne(c.Context(), id, map[string]any{"po_date": poDate}, nil); err != nil {
return nil, utils.Internal("Failed to update po_date")
}
purchase.PoDate = &poDate
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
if err != nil {
return nil, utils.Internal("Failed to reload 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
}
func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation.ReceivePurchaseRequest) (*entity.Purchase, error) { func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation.ReceivePurchaseRequest) (*entity.Purchase, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
@@ -35,6 +35,7 @@ type ApproveStaffPurchaseRequest struct {
type ApproveManagerPurchaseRequest struct { type ApproveManagerPurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
PoDate *string `json:"po_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
} }
type ReceivePurchaseItemRequest struct { type ReceivePurchaseItemRequest struct {
@@ -60,6 +61,10 @@ type DeletePurchaseItemsRequest struct {
ItemIDs []uint `json:"item_ids" validate:"required,min=1,dive,gt=0"` ItemIDs []uint `json:"item_ids" validate:"required,min=1,dive,gt=0"`
} }
type UpdatePoDateRequest struct {
PoDate string `json:"po_date" validate:"required,datetime=2006-01-02"`
}
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`