Feat(BE-339): make a report for purchasing supplier

This commit is contained in:
ragilap
2025-12-16 14:42:31 +07:00
parent 062a7937e2
commit cd739f41b9
8 changed files with 474 additions and 6 deletions
@@ -97,3 +97,53 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
Data: result,
})
}
func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error {
query := &validation.PurchaseSupplierQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
AreaId: int64(ctx.QueryInt("area_id", 0)),
SupplierId: int64(ctx.QueryInt("supplier_id", 0)),
ProductId: int64(ctx.QueryInt("product_id", 0)),
ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)),
DateFrom: ctx.Query("date_from", ""),
DateTo: ctx.Query("date_to", ""),
SortBy: ctx.Query("sort_by", ""),
FilterBy: ctx.Query("filter_by", ""),
}
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.GetPurchaseSupplier(ctx, query)
if err != nil {
return err
}
filters := map[string]interface{}{
"area_id": query.AreaId,
"supplier_id": query.SupplierId,
"product_id": query.ProductId,
"product_category_id": query.ProductCategoryId,
"date_from": query.DateFrom,
"date_to": query.DateTo,
"sort_by": query.SortBy,
"filter_by": query.FilterBy,
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PurchaseSupplierDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get supplier purchase recap successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
Filters: filters,
},
Data: result,
})
}
@@ -0,0 +1,138 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
)
type PurchaseSupplierRowDTO struct {
ReceiveDate string `json:"receive_date"`
PoDate string `json:"po_date"`
PoNumber string `json:"po_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
PurchaseValue float64 `json:"purchase_value"`
TransportUnitPrice float64 `json:"transport_unit_price"`
TransportValue float64 `json:"transport_value"`
TotalAmount float64 `json:"total_amount"`
Expedition string `json:"expedition"`
DeliveryNumber string `json:"delivery_number"`
}
type PurchaseSupplierSummaryDTO struct {
TotalQty float64 `json:"total_qty"`
TotalPurchaseValue float64 `json:"total_purchase_value"`
TotalTransportValue float64 `json:"total_transport_value"`
TotalAmount float64 `json:"total_amount"`
}
type PurchaseSupplierDTO struct {
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
Rows []PurchaseSupplierRowDTO `json:"rows"`
Summary PurchaseSupplierSummaryDTO `json:"summary"`
}
func formatDatePtr(t *time.Time) string {
if t == nil || t.IsZero() {
return ""
}
return t.Format("02-Jan-2006")
}
func ToPurchaseSupplierRowDTO(item *entity.PurchaseItem) PurchaseSupplierRowDTO {
row := PurchaseSupplierRowDTO{
ReceiveDate: formatDatePtr(item.ReceivedDate),
Qty: item.TotalQty,
UnitPrice: item.Price,
}
if item.Purchase != nil {
row.PoDate = formatDatePtr(item.Purchase.PoDate)
if item.Purchase.PoNumber != nil {
row.PoNumber = *item.Purchase.PoNumber
}
}
if item.Product != nil && item.Product.Id != 0 {
product := productDTO.ToProductRelationDTO(*item.Product)
row.Product = &product
}
if item.Warehouse != nil && item.Warehouse.Id != 0 {
warehouse := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse)
row.Warehouse = &warehouse
}
qty := row.Qty
if qty < 0 {
qty = 0
}
row.PurchaseValue = row.UnitPrice * qty
var transportUnit float64
var expeditionName string
if item.ExpenseNonstock != nil {
transportUnit = item.ExpenseNonstock.Price
if item.ExpenseNonstock.Expense != nil &&
item.ExpenseNonstock.Expense.Supplier != nil &&
item.ExpenseNonstock.Expense.Supplier.Id != 0 {
expSupplier := item.ExpenseNonstock.Expense.Supplier
expeditionName = expSupplier.Name
}
}
row.TransportUnitPrice = transportUnit
row.TransportValue = transportUnit * qty
row.TotalAmount = row.PurchaseValue + row.TransportValue
if expeditionName == "" {
row.Expedition = "-"
} else {
row.Expedition = expeditionName
}
if item.TravelNumber != nil && *item.TravelNumber != "" {
row.DeliveryNumber = *item.TravelNumber
} else {
row.DeliveryNumber = "-"
}
return row
}
func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem) PurchaseSupplierDTO {
var supplierDTORef *supplierDTO.SupplierRelationDTO
if supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(supplier)
supplierDTORef = &mapped
}
rows := make([]PurchaseSupplierRowDTO, 0, len(items))
summary := PurchaseSupplierSummaryDTO{}
for i := range items {
row := ToPurchaseSupplierRowDTO(&items[i])
rows = append(rows, row)
summary.TotalQty += row.Qty
summary.TotalPurchaseValue += row.PurchaseValue
summary.TotalTransportValue += row.TransportValue
summary.TotalAmount += row.TotalAmount
}
return PurchaseSupplierDTO{
Supplier: supplierDTORef,
Rows: rows,
Summary: summary,
}
}
+3 -1
View File
@@ -7,6 +7,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
@@ -20,9 +21,10 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db)
marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc, purchaseSupplierRepository)
RepportRoutes(router, repportService)
}
@@ -0,0 +1,196 @@
package repositories
import (
"context"
"fmt"
"strings"
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"
)
type PurchaseSupplierRepository interface {
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error)
GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error)
}
type purchaseSupplierRepositoryImpl struct {
db *gorm.DB
}
func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository {
return &purchaseSupplierRepositoryImpl{db: db}
}
func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB {
// Tentukan kolom tanggal yang akan dipakai untuk filter
dateColumn := "purchase_items.received_date"
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
case "po_date":
dateColumn = "purchases.po_date"
case "receive_date", "":
dateColumn = "purchase_items.received_date"
}
db := r.db.WithContext(ctx).
Model(&entity.Supplier{}).
Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id")
if filters.SupplierId > 0 {
db = db.Where("suppliers.id = ?", filters.SupplierId)
}
if filters.ProductId > 0 {
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
}
if filters.ProductCategoryId > 0 {
db = db.
Joins("JOIN products ON products.id = purchase_items.product_id").
Where("products.product_category_id = ?", filters.ProductCategoryId)
}
if filters.AreaId > 0 {
db = db.
Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id").
Where("warehouses.area_id = ?", filters.AreaId)
}
if filters.DateFrom != "" {
if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil {
db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom)
}
}
if filters.DateTo != "" {
if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil {
db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo)
}
}
return db
}
func (r *purchaseSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) {
query := r.baseSupplierQuery(ctx, filters)
var totalSuppliers int64
if err := query.
Distinct("suppliers.id").
Count(&totalSuppliers).Error; err != nil {
return nil, 0, err
}
if totalSuppliers == 0 {
return []entity.Supplier{}, 0, nil
}
if offset < 0 {
offset = 0
}
var supplierIDs []uint
if err := query.
Select("suppliers.id").
Order("suppliers.id ASC").
Offset(offset).
Limit(limit).
Pluck("suppliers.id", &supplierIDs).Error; err != nil {
return nil, 0, err
}
if len(supplierIDs) == 0 {
return []entity.Supplier{}, totalSuppliers, nil
}
var suppliers []entity.Supplier
if err := r.db.WithContext(ctx).
Where("id IN ?", supplierIDs).
Find(&suppliers).Error; err != nil {
return nil, 0, err
}
return suppliers, totalSuppliers, nil
}
func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) {
if len(supplierIDs) == 0 {
return []entity.PurchaseItem{}, nil
}
// Tentukan kolom tanggal yang akan dipakai untuk filter & sort
dateColumn := "purchase_items.received_date"
switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) {
case "po_date":
dateColumn = "purchases.po_date"
case "receive_date", "":
dateColumn = "purchase_items.received_date"
}
orderDirection := "ASC"
switch strings.ToUpper(strings.TrimSpace(filters.SortBy)) {
case "DESC":
orderDirection = "DESC"
case "ASC", "":
orderDirection = "ASC"
}
db := r.db.WithContext(ctx).
Model(&entity.PurchaseItem{}).
Preload("Purchase").
Preload("Purchase.Supplier").
Preload("Product").
Preload("Product.ProductCategory").
Preload("Warehouse").
Preload("Warehouse.Area").
Preload("Warehouse.Location").
Preload("Warehouse.Kandang").
Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Expense").
Preload("ExpenseNonstock.Expense.Supplier").
Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id").
Where("purchases.supplier_id IN ?", supplierIDs)
if filters.ProductId > 0 {
db = db.Where("purchase_items.product_id = ?", filters.ProductId)
}
if filters.ProductCategoryId > 0 {
db = db.
Joins("JOIN products ON products.id = purchase_items.product_id").
Where("products.product_category_id = ?", filters.ProductCategoryId)
}
if filters.AreaId > 0 {
db = db.
Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id").
Where("warehouses.area_id = ?", filters.AreaId)
}
if filters.DateFrom != "" {
if dateFrom, err := utils.ParseDateString(filters.DateFrom); err == nil {
db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom)
}
}
if filters.DateTo != "" {
if dateTo, err := utils.ParseDateString(filters.DateTo); err == nil {
db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo)
}
}
// Urutkan berdasarkan kolom tanggal yang dipilih dan arah sort
db = db.Order(fmt.Sprintf("%s %s", dateColumn, orderDirection)).
Order("purchase_items.id ASC")
var items []entity.PurchaseItem
if err := db.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
+1
View File
@@ -14,4 +14,5 @@ func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
route.Get("/expense", ctrl.GetExpense)
route.Get("/marketing", ctrl.GetMarketing)
route.Get("/purchase-supplier", ctrl.GetPurchaseSupplier)
}
@@ -2,6 +2,7 @@ package service
import (
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -10,6 +11,8 @@ import (
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
@@ -19,6 +22,7 @@ import (
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)
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
}
type repportService struct {
@@ -27,15 +31,23 @@ type repportService struct {
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
}
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService {
func NewRepportService(
validate *validator.Validate,
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
) RepportService {
return &repportService{
Log: utils.Log,
Validate: validate,
ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo,
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
}
}
@@ -113,3 +125,58 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil
}
func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
if offset < 0 {
offset = 0
}
suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
if err != nil {
return nil, 0, err
}
if totalSuppliers == 0 || len(suppliers) == 0 {
return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil
}
supplierMap := make(map[uint]entity.Supplier, len(suppliers))
supplierIDs := make([]uint, 0, len(suppliers))
for _, supplier := range suppliers {
supplierMap[supplier.Id] = supplier
supplierIDs = append(supplierIDs, supplier.Id)
}
items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
itemsBySupplier := make(map[uint][]entity.PurchaseItem)
for _, item := range items {
if item.Purchase == nil {
continue
}
supplierID := item.Purchase.SupplierId
itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item)
}
result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs))
for _, supplierID := range supplierIDs {
supplier, exists := supplierMap[supplierID]
if !exists {
continue
}
supplierItems := itemsBySupplier[supplierID]
dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems)
result = append(result, dtoItem)
}
return result, totalSuppliers, nil
}
@@ -27,3 +27,16 @@ type MarketingQuery struct {
SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"`
MarketingId int64 `query:"marketing_id" validate:"omitempty"`
}
type PurchaseSupplierQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
AreaId int64 `query:"area_id" validate:"omitempty"`
SupplierId int64 `query:"supplier_id" validate:"omitempty"`
ProductId int64 `query:"product_id" validate:"omitempty"`
ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"`
DateFrom string `query:"date_from" validate:"omitempty"`
DateTo string `query:"date_to" validate:"omitempty"`
SortBy string `query:"sort_by" validate:"omitempty"`
FilterBy string `query:"filter_by" validate:"omitempty"`
}
+1
View File
@@ -18,6 +18,7 @@ type Meta struct {
Limit int `json:"limit"`
TotalPages int64 `json:"total_pages"`
TotalResults int64 `json:"total_results"`
Filters interface{} `json:"filters,omitempty"`
}
type SuccessWithPaginate[T any] struct {