mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' into 'staging'
Development See merge request mbugroup/lti-api!323
This commit is contained in:
@@ -21,6 +21,7 @@ type PurchaseItem struct {
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
ExpenseNonstockId *uint64
|
||||
HasChickin bool `gorm:"-" json:"-"`
|
||||
|
||||
// Relations
|
||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||
|
||||
+37
-7
@@ -100,11 +100,15 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
var marketingTypes []string
|
||||
if params.Type != "" {
|
||||
if !utils.IsValidMarketingType(params.Type) {
|
||||
marketingTypes = utils.ParseQueryArray(params.Type)
|
||||
for _, t := range marketingTypes {
|
||||
if !utils.IsValidMarketingType(t) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanFlags := utils.ParseFlags(params.Flags)
|
||||
|
||||
@@ -135,16 +139,42 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||
}
|
||||
|
||||
if params.Type != "" {
|
||||
switch params.Type {
|
||||
if len(marketingTypes) > 0 {
|
||||
flagSet := make(map[string]struct{})
|
||||
for _, t := range marketingTypes {
|
||||
switch t {
|
||||
case string(utils.MarketingTypeAyamPullet):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)})
|
||||
flagSet[string(utils.FlagDOC)] = struct{}{}
|
||||
flagSet[string(utils.FlagPullet)] = struct{}{}
|
||||
flagSet[string(utils.FlagLayer)] = struct{}{}
|
||||
case string(utils.MarketingTypeAyam):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati)})
|
||||
flagSet[string(utils.FlagAyamAfkir)] = struct{}{}
|
||||
flagSet[string(utils.FlagAyamCulling)] = struct{}{}
|
||||
flagSet[string(utils.FlagAyamMati)] = struct{}{}
|
||||
case string(utils.MarketingTypeTelur):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak)})
|
||||
flagSet[string(utils.FlagTelur)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurUtuh)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurPecah)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurPutih)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurRetak)] = struct{}{}
|
||||
case string(utils.MarketingTypeTrading):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher), string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), string(utils.FlagEkspedisi)})
|
||||
flagSet[string(utils.FlagPakan)] = struct{}{}
|
||||
flagSet[string(utils.FlagPreStarter)] = struct{}{}
|
||||
flagSet[string(utils.FlagStarter)] = struct{}{}
|
||||
flagSet[string(utils.FlagFinisher)] = struct{}{}
|
||||
flagSet[string(utils.FlagOVK)] = struct{}{}
|
||||
flagSet[string(utils.FlagObat)] = struct{}{}
|
||||
flagSet[string(utils.FlagVitamin)] = struct{}{}
|
||||
flagSet[string(utils.FlagKimia)] = struct{}{}
|
||||
flagSet[string(utils.FlagEkspedisi)] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(flagSet) > 0 {
|
||||
flags := make([]string, 0, len(flagSet))
|
||||
for f := range flagSet {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
db = s.Repository.ApplyFlagsFilter(db, flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -20,5 +20,5 @@ type Query struct {
|
||||
Flags string `query:"flags" validate:"omitempty"`
|
||||
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
||||
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
|
||||
Type string `query:"type" validate:"omitempty,oneof=AYAM TELUR TRADING AYAM_PULLET"`
|
||||
Type string `query:"type" validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -692,6 +692,13 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionRejected {
|
||||
note := fmt.Sprintf("Recording-Reject#%d", id)
|
||||
if err := s.rollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -729,43 +736,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
note := fmt.Sprintf("Recording-Delete#%d", id)
|
||||
|
||||
return s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
oldDepletions, err := s.Repository.ListDepletions(tx, id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list depletions before delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
if s.FifoSvc != nil {
|
||||
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
oldEggs, err := s.Repository.ListEggs(tx, id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list eggs before delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
if s.FifoSvc != nil {
|
||||
if err := ensureRecordingEggsUnused(oldEggs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
oldStocks, err := s.Repository.ListStocks(tx, id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list stocks before delete: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.releaseRecordingStocks(ctx, tx, oldStocks, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldEggs, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.logRecordingEggRollback(ctx, tx, oldEggs, note, actorID); err != nil {
|
||||
if err := s.rollbackRecordingInventory(ctx, tx, id, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -781,6 +752,51 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *recordingService) rollbackRecordingInventory(ctx context.Context, tx *gorm.DB, recordingID uint, note string, actorID uint) error {
|
||||
if recordingID == 0 || tx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldDepletions, err := s.Repository.ListDepletions(tx, recordingID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list depletions: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
oldEggs, err := s.Repository.ListEggs(tx, recordingID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list eggs: %+v", err)
|
||||
return err
|
||||
}
|
||||
if s.FifoSvc != nil {
|
||||
if err := ensureRecordingEggsUnused(oldEggs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.releaseRecordingDepletions(ctx, tx, oldDepletions, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
oldStocks, err := s.Repository.ListStocks(tx, recordingID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to list stocks: %+v", err)
|
||||
return err
|
||||
}
|
||||
if err := s.releaseRecordingStocks(ctx, tx, oldStocks, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldEggs, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.logRecordingEggRollback(ctx, tx, oldEggs, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// === Persistence Helpers ===
|
||||
|
||||
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion, eggs []validation.Egg) error {
|
||||
|
||||
@@ -67,6 +67,7 @@ type PurchaseItemDTO struct {
|
||||
VehicleNumber *string `json:"vehicle_number"`
|
||||
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
|
||||
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
|
||||
HasChickin bool `json:"has_chickin"`
|
||||
}
|
||||
|
||||
type PoExpeditionDTO struct {
|
||||
@@ -100,6 +101,7 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||
TravelNumber: item.TravelNumber,
|
||||
TravelDocumentPath: item.TravelNumberDocs,
|
||||
VehicleNumber: item.VehicleNumber,
|
||||
HasChickin: item.HasChickin,
|
||||
}
|
||||
|
||||
if item.Product != nil && item.Product.Id != 0 {
|
||||
|
||||
@@ -255,6 +255,39 @@ func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
if len(purchase.Items) > 0 {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(c.Context()).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for i := range purchase.Items {
|
||||
if _, ok := usedSet[purchase.Items[i].Id]; ok {
|
||||
purchase.Items[i].HasChickin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.applyTravelDocumentURLs(c.Context(), purchase)
|
||||
|
||||
return purchase, nil
|
||||
@@ -498,6 +531,54 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
return nil, utils.BadRequest("Items must not be empty for staff approval")
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionApproved {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
itemByID[purchase.Items[i].Id] = purchase.Items[i]
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(usedIDs) > 0 {
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for _, payload := range req.Items {
|
||||
if payload.PurchaseItemID == 0 || payload.Qty == nil {
|
||||
continue
|
||||
}
|
||||
if _, used := usedSet[payload.PurchaseItemID]; !used {
|
||||
continue
|
||||
}
|
||||
item, ok := itemByID[payload.PurchaseItemID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if *payload.Qty != item.SubQty {
|
||||
return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -745,6 +826,54 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
req.Items[idx].TravelDocumentPath = &uploadedURL
|
||||
}
|
||||
}
|
||||
if action == entity.ApprovalActionApproved {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
itemByID[purchase.Items[i].Id] = purchase.Items[i]
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(usedIDs) > 0 {
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for _, payload := range req.Items {
|
||||
if _, used := usedSet[payload.PurchaseItemID]; !used {
|
||||
continue
|
||||
}
|
||||
item, ok := itemByID[payload.PurchaseItemID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
receivedQty := item.SubQty
|
||||
if payload.ReceivedQty != nil {
|
||||
receivedQty = *payload.ReceivedQty
|
||||
}
|
||||
if receivedQty != item.TotalQty {
|
||||
return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
@@ -1367,6 +1496,30 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
itemIDs := make([]uint, 0, len(itemsToDelete))
|
||||
for _, item := range itemsToDelete {
|
||||
if item.Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, item.Id)
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.StockAllocation{}).
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return utils.BadRequest("Purchase already chickin, failed to delete purchase")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1383,6 +1536,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
var fe *fiber.Error
|
||||
if errors.As(transactionErr, &fe) {
|
||||
return fe
|
||||
}
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return utils.NotFound("Purchase not found")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user