Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into Feat/BE/Expense_adjust_approval_flow

This commit is contained in:
aguhh18
2026-01-11 21:19:35 +07:00
32 changed files with 507 additions and 205 deletions
@@ -101,20 +101,25 @@ func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
func partyFromInitial(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
Id: e.PartyId,
Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
@@ -120,6 +120,7 @@ func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
TransactionType: string(utils.TransactionTypeSaldoAwal),
PartyType: party,
PartyId: req.PartyId,
PartyAccountNumber: nil,
PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
@@ -106,6 +106,7 @@ func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
TransactionType: string(utils.TransactionTypeInjection),
PartyType: string(utils.PaymentPartyCustomer),
PartyId: 0,
PartyAccountNumber: nil,
PaymentDate: adjustmentDate,
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
@@ -1,17 +1,17 @@
package validation
type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
}
type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type Query struct {
@@ -124,20 +124,25 @@ func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
func partyFromPayment(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
Id: e.PartyId,
Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
+3 -3
View File
@@ -15,7 +15,7 @@ func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService
route := v1.Group("/payments")
route.Use(m.Auth(u))
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
route.Post("/", m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
}
@@ -121,18 +121,19 @@ func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
}
createBody := &entity.Payment{
PaymentCode: code,
ReferenceNumber: req.ReferenceNumber,
TransactionType: transactionType,
PartyType: party,
PartyId: req.PartyId,
PaymentDate: paymentDate,
PaymentMethod: method,
BankId: req.BankId,
Direction: directionForParty(party),
Nominal: req.Nominal,
Notes: req.Notes,
CreatedBy: actorID,
PaymentCode: code,
ReferenceNumber: req.ReferenceNumber,
TransactionType: transactionType,
PartyType: party,
PartyId: req.PartyId,
PartyAccountNumber: req.PartyAccountNumber,
PaymentDate: paymentDate,
PaymentMethod: method,
BankId: req.BankId,
Direction: directionForParty(party),
Nominal: req.Nominal,
Notes: req.Notes,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -188,6 +189,9 @@ func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber
}
if req.PartyAccountNumber != nil {
updateBody["party_account_number"] = *req.PartyAccountNumber
}
if req.PaymentMethod != nil {
method, err := normalizePaymentMethod(*req.PaymentMethod)
if err != nil {
@@ -1,25 +1,27 @@
package validation
type Create struct {
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
Nominal float64 `json:"nominal" validate:"required_strict"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
PartyAccountNumber *string `json:"party_account_number"`
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
Nominal float64 `json:"nominal" validate:"required_strict"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
}
type Update struct {
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
PartyAccountNumber *string `json:"party_account_number,omitempty"`
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type Query struct {
@@ -124,20 +124,25 @@ func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
func partyFromPayment(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
Id: e.PartyId,
Type: e.PartyType,
}
if e.PartyAccountNumber != nil {
party.AccountNumber = *e.PartyAccountNumber
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
if party.AccountNumber == "" {
party.AccountNumber = e.Customer.AccountNumber
}
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
if party.AccountNumber == "" && e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
@@ -380,3 +381,38 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Conte
}
return &product, nil
}
func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) {
if len(prefixes) == 0 {
return []uint{}, nil
}
db := r.DB().WithContext(ctx).
Model(&entity.Product{}).
Distinct("products.id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct)
applied := false
for _, prefix := range prefixes {
if prefix == "" {
continue
}
like := prefix + "%"
if !applied {
db = db.Where("flags.name LIKE ?", like)
applied = true
continue
}
db = db.Or("flags.name LIKE ?", like)
}
if visibleStatus != nil {
db = db.Where("products.is_visible = ?", *visibleStatus)
}
var ids []uint
if err := db.Pluck("products.id", &ids).Error; err != nil {
return nil, err
}
return ids, nil
}
@@ -5,6 +5,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
@@ -19,6 +20,7 @@ type ProductRelationDTO struct {
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
}
type ProductListDTO struct {
@@ -33,6 +35,7 @@ type ProductListDTO struct {
Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -70,6 +73,7 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
Flags: &flags,
Uom: uomRef,
ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
}
}
@@ -112,6 +116,7 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
ProductCategory: categoryRef,
Suppliers: toProductSupplierDTOs(e.ProductSuppliers),
}
}
@@ -128,3 +133,23 @@ func ToProductDetailDTO(e entity.Product) ProductDetailDTO {
ProductListDTO: ToProductListDTO(e),
}
}
func toProductSupplierDTOs(relations []entity.ProductSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
for _, relation := range relations {
if relation.Supplier.Id == 0 {
continue
}
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
}
if len(result) == 0 {
return make([]supplierDTO.SupplierRelationDTO, 0)
}
return result
}
@@ -23,6 +23,7 @@ import (
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -308,12 +309,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
}
createBody := &entity.ProjectFlock{
AreaId: req.AreaId,
Category: cat,
FcrId: req.FcrId,
AreaId: req.AreaId,
Category: cat,
FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId,
CreatedBy: actorID,
LocationId: req.LocationId,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -823,22 +824,7 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil
}
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs)
if err != nil {
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
}
if len(blocked) > 0 {
names := make([]string, 0, len(blocked))
for _, item := range blocked {
label := fmt.Sprintf("ID %d", item.Id)
if strings.TrimSpace(item.Name) != "" {
label = fmt.Sprintf("%s (%s)", label, item.Name)
}
names = append(names, label)
}
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
}
// NOTE: Recording constraints are enforced via FK cascade; allow detachment even if recordings exist.
pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs)
if err != nil {
@@ -854,6 +840,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
}
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
purchaseRepo := purchaseRepository.NewPurchaseRepository(db)
if err := purchaseRepo.SoftDeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to soft delete purchases for project flock kandang")
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -906,6 +900,11 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return nil
}
projectFlockID := records[0].ProjectFlockId
if projectFlockID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock id tidak ditemukan")
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -920,24 +919,34 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB())
}
flags := []utils.FlagType{
utils.FlagAyamAfkir,
utils.FlagAyamCulling,
utils.FlagAyamMati,
utils.FlagTelurPecah,
utils.FlagTelurUtuh,
db := s.Repository.DB()
if dbTransaction != nil {
db = dbTransaction
}
var category string
if err := db.WithContext(ctx).
Model(&entity.ProjectFlock{}).
Select("category").
Where("id = ?", projectFlockID).
Scan(&category).Error; err != nil {
return err
}
if strings.TrimSpace(category) == "" {
return fiber.NewError(fiber.StatusBadRequest, "Project flock category tidak ditemukan")
}
productIDs := make(map[utils.FlagType]uint, len(flags))
for _, flag := range flags {
product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag))
}
return err
}
productIDs[flag] = product.Id
prefixes := []string{"AYAM-"}
if strings.EqualFold(category, string(utils.ProjectFlockCategoryLaying)) {
prefixes = append(prefixes, "TELUR")
}
invisibleOnly := false
productIDs, err := pwRepo.ListProductIDsByFlagPrefixes(ctx, prefixes, &invisibleOnly)
if err != nil {
return err
}
if len(productIDs) == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product dengan flag %s tidak ditemukan", strings.Join(prefixes, ", ")))
}
for _, record := range records {
@@ -953,8 +962,7 @@ func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx cont
return err
}
for _, flag := range flags {
productID := productIDs[flag]
for _, productID := range productIDs {
if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil {
continue
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -25,16 +25,16 @@ type RecordingRelationDTO struct {
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"`
HandDay float64 `json:"hand_day"`
HandHouse float64 `json:"hand_house"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMesh float64 `json:"egg_mesh"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
StandardHandDay *float64 `json:"hand_day_std,omitempty"`
StandardHandHouse *float64 `json:"hand_house_std,omitempty"`
StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMesh *float64 `json:"egg_mesh_std,omitempty"`
StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
@@ -94,10 +94,10 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
cumIntake int
fcrValue float64
totalChickQty float64
handDay float64
handHouse float64
henDay float64
henHouse float64
feedIntake float64
eggMesh float64
eggMass float64
eggWeight float64
)
@@ -119,17 +119,17 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty
}
if e.HandDay != nil {
handDay = *e.HandDay
if e.HenDay != nil {
henDay = *e.HenDay
}
if e.HandHouse != nil {
handHouse = *e.HandHouse
if e.HenHouse != nil {
henHouse = *e.HenHouse
}
if e.FeedIntake != nil {
feedIntake = *e.FeedIntake
}
if e.EggMesh != nil {
eggMesh = *e.EggMesh
if e.EggMass != nil {
eggMass = *e.EggMass
}
if e.EggWeight != nil {
eggWeight = *e.EggWeight
@@ -157,16 +157,16 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
CumIntake: cumIntake,
FcrValue: fcrValue,
TotalChickQty: totalChickQty,
HandDay: handDay,
HandHouse: handHouse,
HenDay: henDay,
HenHouse: henHouse,
FeedIntake: feedIntake,
EggMesh: eggMesh,
EggMass: eggMass,
EggWeight: eggWeight,
StandardHandDay: e.StandardHandDay,
StandardHandHouse: e.StandardHandHouse,
StandardHenDay: e.StandardHenDay,
StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMesh: e.StandardEggMesh,
StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr,
Approval: latestApproval,
@@ -16,10 +16,10 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne)
route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne)
route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne)
route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay)
route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve)
}
@@ -901,47 +901,6 @@ type eggTotals struct {
Weight float64
}
type stockTotals struct {
Usage float64
Pending float64
Total float64
}
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += usage
current.Pending += pending
current.Total += usage + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += stock.Qty
current.Pending += pending
current.Total += stock.Qty + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false
@@ -1156,34 +1115,34 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
recording.FeedIntake = nil
}
var handDay float64
var henDay float64
if remainingChick > 0 && totalEggQty >= 0 {
handDay = (totalEggQty / remainingChick) * 100
updates["hand_day"] = handDay
recording.HandDay = &handDay
henDay = (totalEggQty / remainingChick) * 100
updates["hen_day"] = henDay
recording.HenDay = &henDay
} else {
updates["hand_day"] = gorm.Expr("NULL")
recording.HandDay = nil
updates["hen_day"] = gorm.Expr("NULL")
recording.HenDay = nil
}
var handHouse float64
var henHouse float64
if initialChickin > 0 && cumulativeEggQty >= 0 {
handHouse = cumulativeEggQty / initialChickin
updates["hand_house"] = handHouse
recording.HandHouse = &handHouse
henHouse = cumulativeEggQty / initialChickin
updates["hen_house"] = henHouse
recording.HenHouse = &henHouse
} else {
updates["hand_house"] = gorm.Expr("NULL")
recording.HandHouse = nil
updates["hen_house"] = gorm.Expr("NULL")
recording.HenHouse = nil
}
var eggMesh float64
var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMesh = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mesh"] = eggMesh
recording.EggMesh = &eggMesh
eggMass = (totalEggWeightGrams / remainingChick) * 1000
updates["egg_mass"] = eggMass
recording.EggMass = &eggMass
} else {
updates["egg_mesh"] = gorm.Expr("NULL")
recording.EggMesh = nil
updates["egg_mass"] = gorm.Expr("NULL")
recording.EggMass = nil
}
var eggWeight float64
@@ -1334,11 +1293,11 @@ func (s *recordingService) attachLatestApproval(ctx context.Context, item *entit
}
type productionStandardValues struct {
HandDay *float64
HandHouse *float64
HenDay *float64
HenHouse *float64
FeedIntake *float64
MaxDepletion *float64
EggMesh *float64
EggMass *float64
EggWeight *float64
}
@@ -1389,10 +1348,10 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
return err
}
if detail != nil {
standard.HandDay = detail.TargetHenDayProduction
standard.HandHouse = detail.TargetHenHouseProduction
standard.HenDay = detail.TargetHenDayProduction
standard.HenHouse = detail.TargetHenHouseProduction
standard.EggWeight = detail.TargetEggWeight
standard.EggMesh = detail.TargetEggMass
standard.EggMass = detail.TargetEggMass
}
}
@@ -1420,11 +1379,11 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e
}
}
item.StandardHandDay = standard.HandDay
item.StandardHandHouse = standard.HandHouse
item.StandardHenDay = standard.HenDay
item.StandardHenHouse = standard.HenHouse
item.StandardFeedIntake = standard.FeedIntake
item.StandardMaxDepletion = standard.MaxDepletion
item.StandardEggMesh = standard.EggMesh
item.StandardEggMass = standard.EggMass
item.StandardEggWeight = standard.EggWeight
item.StandardFcr = standardFcr
@@ -25,6 +25,7 @@ type PurchaseRepository interface {
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
}
@@ -89,6 +90,44 @@ WHERE pi.purchase_id = ?
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
}
func (r *PurchaseRepositoryImpl) SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
if len(projectFlockKandangIDs) == 0 {
return nil
}
return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var purchaseIDs []uint
query := `
SELECT pi.purchase_id
FROM purchase_items pi
WHERE pi.project_flock_kandang_id IN (?)
GROUP BY pi.purchase_id
HAVING COUNT(*) = COUNT(CASE WHEN pi.project_flock_kandang_id IN (?) THEN 1 END)
`
if err := tx.Raw(query, projectFlockKandangIDs, projectFlockKandangIDs).Scan(&purchaseIDs).Error; err != nil {
return err
}
now := time.Now().UTC()
if len(purchaseIDs) > 0 {
if err := tx.Model(&entity.Purchase{}).
Where("id IN (?) AND deleted_at IS NULL", purchaseIDs).
Update("deleted_at", now).Error; err != nil {
return err
}
if err := tx.Where("purchase_id IN (?)", purchaseIDs).Delete(&entity.PurchaseItem{}).Error; err != nil {
return err
}
}
deleteItems := tx.Where("project_flock_kandang_id IN (?)", projectFlockKandangIDs)
if len(purchaseIDs) > 0 {
deleteItems = deleteItems.Where("purchase_id NOT IN (?)", purchaseIDs)
}
return deleteItems.Delete(&entity.PurchaseItem{}).Error
})
}
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
if len(items) == 0 {
return nil
@@ -133,6 +133,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db = db.Where("purchases.deleted_at IS NULL")
if params.SupplierID > 0 {
db = db.Where("supplier_id = ?", params.SupplierID)