mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
update purchase triger to expense
This commit is contained in:
@@ -47,6 +47,10 @@ type groupedItem struct {
|
|||||||
totalPrice float64
|
totalPrice float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
||||||
|
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
// expenseBridge is the real implementation that syncs purchases to expenses on receiving/deletion.
|
// expenseBridge is the real implementation that syncs purchases to expenses on receiving/deletion.
|
||||||
type expenseBridge struct {
|
type expenseBridge struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
@@ -232,6 +236,33 @@ func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupEmptyExpenses deletes expense headers (and approvals) that have no nonstocks.
|
||||||
|
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) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
|
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
|
||||||
if purchaseID == 0 || len(updates) == 0 {
|
if purchaseID == 0 || len(updates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -260,6 +291,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemLinks := make(map[uint]itemLink)
|
itemLinks := make(map[uint]itemLink)
|
||||||
|
existingExpenseByKey := make(map[string]uint64)
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
ids := make([]uint, 0, len(updates))
|
ids := make([]uint, 0, len(updates))
|
||||||
for _, upd := range updates {
|
for _, upd := range updates {
|
||||||
@@ -286,6 +318,7 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Build quick lookup per item and per group key for existing expenses.
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
itemLinks[row.ItemID] = itemLink{
|
itemLinks[row.ItemID] = itemLink{
|
||||||
ExpenseNonstockID: row.ExpenseNonstockID,
|
ExpenseNonstockID: row.ExpenseNonstockID,
|
||||||
@@ -295,6 +328,16 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
Qty: row.Qty,
|
Qty: row.Qty,
|
||||||
Price: row.Price,
|
Price: row.Price,
|
||||||
}
|
}
|
||||||
|
if row.ExpenseID != 0 && row.SupplierID != 0 && !row.TransactionDate.IsZero() {
|
||||||
|
// Use warehouse from purchase item; if not found, skip key.
|
||||||
|
for i := range purchase.Items {
|
||||||
|
if purchase.Items[i].Id == row.ItemID {
|
||||||
|
key := groupingKey(row.SupplierID, row.TransactionDate.UTC().Truncate(24*time.Hour), purchase.Items[i].WarehouseId)
|
||||||
|
existingExpenseByKey[key] = row.ExpenseID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,6 +350,8 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
groups := make(map[string][]groupedItem)
|
groups := make(map[string][]groupedItem)
|
||||||
toRecreate := make([]ExpenseReceivingPayload, 0)
|
toRecreate := make([]ExpenseReceivingPayload, 0)
|
||||||
|
|
||||||
|
movedFrom := make([]uint64, 0)
|
||||||
|
|
||||||
for _, payload := range updates {
|
for _, payload := range updates {
|
||||||
if payload.ReceivedDate == nil {
|
if payload.ReceivedDate == nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
||||||
@@ -338,40 +383,31 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
pricePerItem = *payload.TransportPerItem
|
pricePerItem = *payload.TransportPerItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any change other than received_date / supplier occurs (e.g., price or qty), delete-then-create.
|
// Supplier/date change: prefer re-link to existing expense in target group; otherwise recreate.
|
||||||
if (link.Price != pricePerItem) || (link.Qty != payload.ReceivedQty) {
|
if oldSupplier != supplierID || !oldDate.Equal(newDate) {
|
||||||
requiresDelete = true
|
newKey := groupingKey(supplierID, newDate, payload.WarehouseID)
|
||||||
} else if oldSupplier != supplierID || !oldDate.Equal(newDate) {
|
if targetExpenseID, ok := existingExpenseByKey[newKey]; ok && targetExpenseID != 0 {
|
||||||
// Supplier/date change: keep (update) if this expense only has one nonstock; otherwise recreate to avoid affecting others.
|
// Move nonstock to existing expense header in the target group.
|
||||||
var count int64
|
|
||||||
if err := b.db.WithContext(ctx).
|
|
||||||
Model(&entity.ExpenseNonstock{}).
|
|
||||||
Where("expense_id = ?", link.ExpenseID).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count <= 1 {
|
|
||||||
// Update expense header supplier/date in-place.
|
|
||||||
if err := b.db.WithContext(ctx).
|
|
||||||
Model(&entity.Expense{}).
|
|
||||||
Where("id = ?", link.ExpenseID).
|
|
||||||
Updates(map[string]interface{}{
|
|
||||||
"supplier_id": supplierID,
|
|
||||||
"transaction_date": newDate,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Update note just in case.
|
|
||||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
|
pricePerItem := item.Price
|
||||||
|
if payload.TransportPerItem != nil {
|
||||||
|
pricePerItem = *payload.TransportPerItem
|
||||||
|
}
|
||||||
if err := b.db.WithContext(ctx).
|
if err := b.db.WithContext(ctx).
|
||||||
Model(&entity.ExpenseNonstock{}).
|
Model(&entity.ExpenseNonstock{}).
|
||||||
Where("id = ?", link.ExpenseNonstockID).
|
Where("id = ?", link.ExpenseNonstockID).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"notes": note,
|
"expense_id": targetExpenseID,
|
||||||
|
"qty": payload.ReceivedQty,
|
||||||
|
"price": pricePerItem,
|
||||||
|
"notes": note,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Continue to grouping with updated header.
|
// Track cleanup for old header if it becomes empty.
|
||||||
|
movedFrom = append(movedFrom, link.ExpenseID)
|
||||||
|
existingExpenseByKey[newKey] = targetExpenseID
|
||||||
|
handledUpdate = true
|
||||||
} else {
|
} else {
|
||||||
requiresDelete = true
|
requiresDelete = true
|
||||||
}
|
}
|
||||||
@@ -379,10 +415,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
|
|
||||||
// If we reach here and no delete is required, update the existing nonstock fields and skip creation.
|
// If we reach here and no delete is required, update the existing nonstock fields and skip creation.
|
||||||
if !requiresDelete {
|
if !requiresDelete {
|
||||||
pricePerItem := item.Price
|
|
||||||
if payload.TransportPerItem != nil {
|
|
||||||
pricePerItem = *payload.TransportPerItem
|
|
||||||
}
|
|
||||||
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
if err := b.db.WithContext(ctx).
|
if err := b.db.WithContext(ctx).
|
||||||
Model(&entity.ExpenseNonstock{}).
|
Model(&entity.ExpenseNonstock{}).
|
||||||
@@ -511,6 +543,13 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup old expense headers that became empty after re-link.
|
||||||
|
if len(movedFrom) > 0 {
|
||||||
|
if err := b.cleanupEmptyExpenses(ctx, movedFrom); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
createdFrom, createdTo, err := parseQueryDates(params.CreatedFrom, params.CreatedTo)
|
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -233,9 +233,9 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
if warehouse, ok := warehouseCache[id]; ok {
|
if warehouse, ok := warehouseCache[id]; ok {
|
||||||
return warehouse, nil
|
return warehouse, nil
|
||||||
}
|
}
|
||||||
warehouse, err := s.WarehouseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
warehouse, err := s.WarehouseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("Area").Preload("Location")
|
return db.Preload("Area").Preload("Location")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -299,22 +299,22 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
|
|
||||||
purchase := &entity.Purchase{
|
purchase := &entity.Purchase{
|
||||||
SupplierId: uint(req.SupplierID),
|
SupplierId: uint(req.SupplierID),
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
CreatedBy: uint(actorID),
|
CreatedBy: uint(actorID),
|
||||||
}
|
}
|
||||||
|
|
||||||
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
||||||
emptyVehicle := ""
|
emptyVehicle := ""
|
||||||
for _, item := range aggregated {
|
for _, item := range aggregated {
|
||||||
items = append(items, &entity.PurchaseItem{
|
items = append(items, &entity.PurchaseItem{
|
||||||
ProductId: item.productId,
|
ProductId: item.productId,
|
||||||
WarehouseId: item.warehouseId,
|
WarehouseId: item.warehouseId,
|
||||||
SubQty: item.subQty,
|
SubQty: item.subQty,
|
||||||
TotalQty: 0,
|
TotalQty: 0,
|
||||||
TotalUsed: 0,
|
TotalUsed: 0,
|
||||||
Price: 0,
|
Price: 0,
|
||||||
TotalPrice: 0,
|
TotalPrice: 0,
|
||||||
VehicleNumber: &emptyVehicle,
|
VehicleNumber: &emptyVehicle,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -856,13 +856,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
for _, prep := range prepared {
|
for _, prep := range prepared {
|
||||||
date := prep.receivedDate
|
date := prep.receivedDate
|
||||||
payload := ExpenseReceivingPayload{
|
payload := ExpenseReceivingPayload{
|
||||||
PurchaseItemID: prep.item.Id,
|
PurchaseItemID: prep.item.Id,
|
||||||
ProductID: prep.item.ProductId,
|
ProductID: prep.item.ProductId,
|
||||||
WarehouseID: uint(prep.warehouseID),
|
WarehouseID: uint(prep.warehouseID),
|
||||||
SupplierID: prep.supplierID,
|
SupplierID: prep.supplierID,
|
||||||
TransportPerItem: prep.transportPerItem,
|
TransportPerItem: prep.transportPerItem,
|
||||||
ReceivedQty: prep.receivedQty,
|
ReceivedQty: prep.receivedQty,
|
||||||
ReceivedDate: &date,
|
ReceivedDate: &date,
|
||||||
}
|
}
|
||||||
receivingPayloads = append(receivingPayloads, payload)
|
receivingPayloads = append(receivingPayloads, payload)
|
||||||
}
|
}
|
||||||
@@ -1090,49 +1090,6 @@ func (s *purchaseService) approvalServiceForDB(db *gorm.DB) commonSvc.ApprovalSe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []entity.Purchase) error {
|
|
||||||
if len(items) == 0 || s.ApprovalSvc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ids := make([]uint, 0, len(items))
|
|
||||||
visited := make(map[uint]struct{}, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
if item.Id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := visited[item.Id]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
visited[item.Id] = struct{}{}
|
|
||||||
ids = append(ids, uint(item.Id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowPurchase, ids, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.Preload("ActionUser")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range items {
|
|
||||||
if items[i].Id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if approval, ok := latestMap[uint(items[i].Id)]; ok {
|
|
||||||
items[i].LatestApproval = approval
|
|
||||||
} else {
|
|
||||||
items[i].LatestApproval = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *purchaseService) notifyExpenseItemsReceived(c *fiber.Ctx, purchaseID uint, payloads []ExpenseReceivingPayload) error {
|
func (s *purchaseService) notifyExpenseItemsReceived(c *fiber.Ctx, purchaseID uint, payloads []ExpenseReceivingPayload) error {
|
||||||
if s.ExpenseBridge == nil || purchaseID == 0 || len(payloads) == 0 {
|
if s.ExpenseBridge == nil || purchaseID == 0 || len(payloads) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1237,9 +1194,9 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
update.TotalQty = &qtyCopy
|
update.TotalQty = &qtyCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
delete(requestItems, item.Id)
|
delete(requestItems, item.Id)
|
||||||
}
|
}
|
||||||
if len(requestItems) > 0 {
|
if len(requestItems) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
|
||||||
}
|
}
|
||||||
@@ -1293,19 +1250,19 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
newItem := &entity.PurchaseItem{
|
newItem := &entity.PurchaseItem{
|
||||||
PurchaseId: purchase.Id,
|
PurchaseId: purchase.Id,
|
||||||
ProductId: payload.ProductID,
|
ProductId: payload.ProductID,
|
||||||
WarehouseId: payload.WarehouseID,
|
WarehouseId: payload.WarehouseID,
|
||||||
SubQty: qty,
|
SubQty: qty,
|
||||||
TotalQty: 0,
|
TotalQty: 0,
|
||||||
TotalUsed: 0,
|
TotalUsed: 0,
|
||||||
Price: payload.Price,
|
Price: payload.Price,
|
||||||
TotalPrice: totalPrice,
|
TotalPrice: totalPrice,
|
||||||
VehicleNumber: &emptyVehicle,
|
VehicleNumber: &emptyVehicle,
|
||||||
}
|
}
|
||||||
newItems = append(newItems, newItem)
|
newItems = append(newItems, newItem)
|
||||||
existingCombos[key] = struct{}{}
|
existingCombos[key] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updates) == 0 && len(newItems) == 0 {
|
if len(updates) == 0 && len(newItems) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process")
|
||||||
@@ -1356,14 +1313,6 @@ func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQueryDates(fromStr, toStr string) (*time.Time, *time.Time, error) {
|
|
||||||
fromPtr, toPtr, err := utils.ParseDateRangeForQuery(fromStr, toStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
return fromPtr, toPtr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
||||||
value := strings.ToUpper(strings.TrimSpace(raw))
|
value := strings.ToUpper(strings.TrimSpace(raw))
|
||||||
switch value {
|
switch value {
|
||||||
|
|||||||
Reference in New Issue
Block a user