mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-25 15:55:44 +00:00
feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto
This commit is contained in:
@@ -3,17 +3,31 @@ package repositories
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PurchaseRepository interface {
|
||||
repository.BaseRepository[entity.Purchase]
|
||||
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
|
||||
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
|
||||
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
|
||||
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
|
||||
WithListRelations() func(*gorm.DB) *gorm.DB
|
||||
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
|
||||
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
}
|
||||
|
||||
type PurchaseRepositoryImpl struct {
|
||||
@@ -26,6 +40,16 @@ func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
type PurchaseListFilter struct {
|
||||
SupplierID uint
|
||||
Search string
|
||||
PrNumber string
|
||||
CreatedFrom *time.Time
|
||||
CreatedTo *time.Time
|
||||
Status *entity.ApprovalAction
|
||||
CompletedOnly bool
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
@@ -47,9 +71,47 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
item.PurchaseId = purchaseID
|
||||
}
|
||||
|
||||
return r.DB().WithContext(ctx).Create(&items).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
|
||||
var purchase entity.Purchase
|
||||
err := r.DB().WithContext(ctx).
|
||||
Scopes(r.withDetailRelations).
|
||||
First(&purchase, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &purchase, nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
|
||||
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = r.withListRelations(db)
|
||||
return r.applyListFilters(db, filter)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return r.withListRelations(db)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("Supplier").
|
||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("id ASC")
|
||||
@@ -58,18 +120,34 @@ func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id ui
|
||||
Preload("Items.Warehouse").
|
||||
Preload("Items.Warehouse.Area").
|
||||
Preload("Items.Warehouse.Location").
|
||||
Preload("Items.ProductWarehouse").
|
||||
First(&purchase, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
Preload("Items.ProductWarehouse")
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return r.withDetailRelations(db)
|
||||
}
|
||||
return &purchase, nil
|
||||
}
|
||||
|
||||
type PurchasePricingUpdate struct {
|
||||
ItemID uint64
|
||||
ProductID *uint64
|
||||
Price float64
|
||||
TotalPrice float64
|
||||
Quantity *float64
|
||||
TotalQty *float64
|
||||
}
|
||||
|
||||
type PurchaseReceivingUpdate struct {
|
||||
ItemID uint64
|
||||
ReceivedDate *time.Time
|
||||
TravelNumber *string
|
||||
TravelDocumentPath *string
|
||||
VehicleNumber *string
|
||||
ReceivedQty *float64
|
||||
WarehouseID *uint
|
||||
ProductWarehouseID *uint
|
||||
ClearProductWarehouse bool
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
@@ -85,13 +163,23 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
for _, upd := range updates {
|
||||
data := map[string]interface{}{
|
||||
"price": upd.Price,
|
||||
"total_price": upd.TotalPrice,
|
||||
}
|
||||
if upd.ProductID != nil {
|
||||
data["product_id"] = *upd.ProductID
|
||||
}
|
||||
if upd.Quantity != nil {
|
||||
data["sub_qty"] = *upd.Quantity
|
||||
}
|
||||
if upd.TotalQty != nil {
|
||||
data["total_qty"] = *upd.TotalQty
|
||||
}
|
||||
|
||||
result := db.Model(&entity.PurchaseItem{}).
|
||||
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||
Updates(map[string]interface{}{
|
||||
"price": upd.Price,
|
||||
"total_price": upd.TotalPrice,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
})
|
||||
Updates(data)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
@@ -111,3 +199,225 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
||||
ctx context.Context,
|
||||
purchaseID uint64,
|
||||
updates []PurchaseReceivingUpdate,
|
||||
) error {
|
||||
if len(updates) == 0 {
|
||||
return errors.New("receiving updates cannot be empty")
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
for _, upd := range updates {
|
||||
data := map[string]interface{}{}
|
||||
|
||||
if upd.ReceivedDate != nil {
|
||||
data["received_date"] = upd.ReceivedDate
|
||||
}
|
||||
if upd.TravelNumber != nil {
|
||||
data["travel_number"] = upd.TravelNumber
|
||||
}
|
||||
if upd.TravelDocumentPath != nil {
|
||||
data["travel_number_docs"] = upd.TravelDocumentPath
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if upd.ProductWarehouseID != nil {
|
||||
data["product_warehouse_id"] = *upd.ProductWarehouseID
|
||||
} else if upd.ClearProductWarehouse {
|
||||
data["product_warehouse_id"] = gorm.Expr("NULL")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
result := db.Model(&entity.PurchaseItem{}).
|
||||
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||
Updates(data)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
|
||||
ctx context.Context,
|
||||
purchaseID uint64,
|
||||
grandTotal float64,
|
||||
) error {
|
||||
return r.DB().WithContext(ctx).
|
||||
Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchaseID).
|
||||
Updates(map[string]interface{}{
|
||||
"grand_total": grandTotal,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
|
||||
if len(itemIDs) == 0 {
|
||||
return errors.New("itemIDs cannot be empty")
|
||||
}
|
||||
|
||||
return r.DB().WithContext(ctx).
|
||||
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
|
||||
Delete(&entity.PurchaseItem{}).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) 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.Purchase{}).
|
||||
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)
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&entity.Purchase{}).
|
||||
Where(fmt.Sprintf("%s = ?", column), value).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
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 *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Supplier")
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
|
||||
if filter == nil {
|
||||
return db
|
||||
}
|
||||
|
||||
if filter.SupplierID > 0 {
|
||||
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
|
||||
}
|
||||
|
||||
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
|
||||
like := "%" + search + "%"
|
||||
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
|
||||
}
|
||||
|
||||
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
|
||||
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
|
||||
}
|
||||
|
||||
if filter.CreatedFrom != nil {
|
||||
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
|
||||
}
|
||||
|
||||
if filter.CreatedTo != nil {
|
||||
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
|
||||
}
|
||||
|
||||
if filter.CompletedOnly {
|
||||
step := uint16(utils.PurchaseStepCompleted)
|
||||
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
|
||||
} else if filter.Status != nil {
|
||||
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
|
||||
}
|
||||
|
||||
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
|
||||
latestSub := r.DB().
|
||||
Model(&entity.Approval{}).
|
||||
Select("approvable_id, MAX(action_at) AS latest_action_at").
|
||||
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
|
||||
Group("approvable_id")
|
||||
|
||||
db = db.
|
||||
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
|
||||
Joins(
|
||||
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
|
||||
utils.ApprovalWorkflowPurchase.String(),
|
||||
).
|
||||
Where("approvals.action = ?", string(action))
|
||||
if minStep != nil {
|
||||
db = db.Where("approvals.step_number >= ?", *minStep)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user