mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
unfinished purchase
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_items;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchase_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
purchase_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NOT NULL,
|
||||||
|
warehouse_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
received_date TIMESTAMPTZ,
|
||||||
|
travel_number VARCHAR,
|
||||||
|
travel_number_docs VARCHAR,
|
||||||
|
vehicle_number VARCHAR,
|
||||||
|
sub_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
total_used NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
price NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
total_price NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product
|
||||||
|
FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_warehouse
|
||||||
|
FOREIGN KEY (warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product_warehouse
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation
|
||||||
|
ON purchase_items (purchase_id, product_id, warehouse_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
AND conrelid = 'purchase_items'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_purchase;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS purchases;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
pr_number VARCHAR NOT NULL,
|
||||||
|
po_number VARCHAR,
|
||||||
|
po_date TIMESTAMPTZ,
|
||||||
|
supplier_id BIGINT NOT NULL,
|
||||||
|
credit_term INT,
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_supplier
|
||||||
|
FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_purchase
|
||||||
|
FOREIGN KEY (purchase_id)
|
||||||
|
REFERENCES purchases(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_pr_number_unique
|
||||||
|
ON purchases (pr_number)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_po_number_unique
|
||||||
|
ON purchases (po_number)
|
||||||
|
WHERE deleted_at IS NULL AND po_number IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Purchase struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PrNumber string `gorm:"not null"`
|
||||||
|
PoNumber *string
|
||||||
|
PoDate *time.Time
|
||||||
|
SupplierId uint64 `gorm:"not null"`
|
||||||
|
CreditTerm *int
|
||||||
|
DueDate *time.Time
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes *string
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedBy uint64 `gorm:"not null"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseItem struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PurchaseId uint64 `gorm:"not null"`
|
||||||
|
ProductId uint64 `gorm:"not null"`
|
||||||
|
WarehouseId uint64 `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint64
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
TravelNumber *string
|
||||||
|
TravelNumberDocs *string
|
||||||
|
VehicleNumber *string
|
||||||
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApprovalBaseDTO struct {
|
type ApprovalBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
StepNumber uint16 `json:"step_number"`
|
StepNumber uint16 `json:"step_number"`
|
||||||
StepName string `json:"step_name"`
|
StepName string `json:"step_name"`
|
||||||
Action *string `json:"action"`
|
Action *string `json:"action"`
|
||||||
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
|
|||||||
|
|
||||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
||||||
dto := ApprovalBaseDTO{
|
dto := ApprovalBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
Notes: e.Notes,
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
|
|||||||
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -149,6 +150,20 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouse entity.ProductWarehouse
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("Warehouse").
|
||||||
|
Preload("Warehouse.Area").
|
||||||
|
Preload("Warehouse.Location").
|
||||||
|
First(&productWarehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &productWarehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
||||||
var product entity.Product
|
var product entity.Product
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type WarehouseRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseRepositoryImpl struct {
|
type WarehouseRepositoryImpl struct {
|
||||||
@@ -62,6 +63,18 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
|
|||||||
return &warehouse, nil
|
return &warehouse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
|
||||||
|
var warehouse entity.Warehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Area").
|
||||||
|
Preload("Location").
|
||||||
|
First(&warehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &warehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
|
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
|
||||||
var warehouse entity.Warehouse
|
var warehouse entity.Warehouse
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
|
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
|
||||||
ctrl := controller.NewProjectflockController(s)
|
ctrl := controller.NewProjectflockController(s)
|
||||||
|
|
||||||
route := v1.Group("/project_flocks")
|
route := v1.Group("/project-flocks")
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
@@ -27,6 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,18 +254,22 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||||
var population entity.ProjectFlockPopulation
|
var total float64
|
||||||
err := tx.
|
err := tx.
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
Table("project_flock_populations").
|
||||||
Order("created_at DESC").
|
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||||
First(&population).Error
|
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
return 0, nil
|
Scan(&total).Error
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return int64(math.Round(population.TotalQty)), nil
|
|
||||||
|
if total < 0 {
|
||||||
|
total = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(math.Round(total)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
|
|||||||
@@ -261,6 +261,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
var recordingEntity *entity.Recording
|
var recordingEntity *entity.Recording
|
||||||
@@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
recordingEntity = recording
|
recordingEntity = recording
|
||||||
|
|
||||||
|
hasBodyChanges := req.BodyWeights != nil
|
||||||
|
hasStockChanges := req.Stocks != nil
|
||||||
|
hasDepletionChanges := req.Depletions != nil
|
||||||
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
|
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var category string
|
var category string
|
||||||
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
|
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
|
||||||
}
|
}
|
||||||
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
||||||
if req.Eggs != nil {
|
if hasEggChanges {
|
||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
@@ -291,7 +304,29 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BodyWeights != nil {
|
if hasStockChanges {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasDepletionChanges || hasEggChanges {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExistingGradings := false
|
||||||
|
for _, egg := range recordingEntity.Eggs {
|
||||||
|
if len(egg.GradingEggs) > 0 {
|
||||||
|
hasExistingGradings = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
|
||||||
|
|
||||||
|
if hasBodyChanges {
|
||||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Stocks != nil {
|
if hasStockChanges {
|
||||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||||
@@ -330,17 +361,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Eggs != nil && req.Depletions == nil {
|
if hasDepletionChanges {
|
||||||
if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Depletions != nil {
|
|
||||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||||
@@ -364,7 +385,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Eggs != nil {
|
if hasEggChanges {
|
||||||
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||||
@@ -386,17 +407,71 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasExistingGradings = false
|
||||||
|
hasEggsAfterUpdate = len(req.Eggs) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
return err
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
action := entity.ApprovalActionUpdated
|
action := entity.ApprovalActionUpdated
|
||||||
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil {
|
actorID := recordingEntity.CreatedBy
|
||||||
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
|
if actorID == 0 {
|
||||||
return err
|
actorID = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var step approvalutils.ApprovalStep
|
||||||
|
if isLaying {
|
||||||
|
if !hasEggsAfterUpdate {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
} else if hasEggChanges {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
} else if hasExistingGradings {
|
||||||
|
step = utils.RecordingStepPengajuan
|
||||||
|
} else {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step = utils.RecordingStepPengajuan
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval := recordingEntity.LatestApproval
|
||||||
|
if latestApproval == nil {
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil {
|
||||||
|
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
} else {
|
||||||
|
latestApproval = fetched
|
||||||
|
}
|
||||||
|
} else if s.ApprovalRepo != nil {
|
||||||
|
if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil {
|
||||||
|
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
} else {
|
||||||
|
latestApproval = fetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCreateApproval := true
|
||||||
|
if latestApproval != nil &&
|
||||||
|
latestApproval.StepNumber == uint16(step) &&
|
||||||
|
latestApproval.Action != nil &&
|
||||||
|
*latestApproval.Action == action {
|
||||||
|
shouldCreateApproval = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreateApproval {
|
||||||
|
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return nil
|
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
|
||||||
}
|
}
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording")
|
if len(populations) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
|
for _, population := range populations {
|
||||||
|
if population.TotalQty > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package validation
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
BodyWeight struct {
|
BodyWeight struct {
|
||||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
||||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||||
TotalWeight float64 `json:"total_weight" validate:"required,gte=0"`
|
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Stock struct {
|
Stock struct {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseController struct {
|
||||||
|
service service.PurchaseService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
||||||
|
return &PurchaseController{service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.CreatePurchaseRequest)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase requisition created successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.ApproveStaffPurchaseRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Staff purchase approval recorded successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SupplierBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreaBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WarehouseBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Area *AreaBaseDTO `json:"area,omitempty"`
|
||||||
|
Location *LocationBaseDTO `json:"location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SKU *string `json:"sku,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Product *ProductBaseDTO `json:"product,omitempty"`
|
||||||
|
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
|
ProductWarehouseID *uint64 `json:"product_warehouse_id,omitempty"`
|
||||||
|
SubQty float64 `json:"sub_qty"`
|
||||||
|
TotalQty float64 `json:"total_qty"`
|
||||||
|
TotalUsed float64 `json:"total_used"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseDetailDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
PrNumber string `json:"pr_number"`
|
||||||
|
Supplier *SupplierBaseDTO `json:"supplier,omitempty"`
|
||||||
|
CreditTerm *int `json:"credit_term,omitempty"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
Items []PurchaseItemDTO `json:"items"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO {
|
||||||
|
if s.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &SupplierBaseDTO{
|
||||||
|
Id: s.Id,
|
||||||
|
Name: s.Name,
|
||||||
|
Alias: s.Alias,
|
||||||
|
Type: s.Type,
|
||||||
|
Category: s.Category,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO {
|
||||||
|
if w == nil || w.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dto := &WarehouseBaseDTO{
|
||||||
|
Id: w.Id,
|
||||||
|
Name: w.Name,
|
||||||
|
}
|
||||||
|
if w.Area.Id != 0 {
|
||||||
|
dto.Area = &AreaBaseDTO{
|
||||||
|
Id: w.Area.Id,
|
||||||
|
Name: w.Area.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.Location != nil && w.Location.Id != 0 {
|
||||||
|
dto.Location = &LocationBaseDTO{
|
||||||
|
Id: w.Location.Id,
|
||||||
|
Name: w.Location.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProductBaseDTO(p *entity.Product) *ProductBaseDTO {
|
||||||
|
if p == nil || p.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dto := &ProductBaseDTO{
|
||||||
|
Id: p.Id,
|
||||||
|
Name: p.Name,
|
||||||
|
}
|
||||||
|
if p.Sku != nil {
|
||||||
|
dto.SKU = p.Sku
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||||
|
dto := PurchaseItemDTO{
|
||||||
|
Id: item.Id,
|
||||||
|
ProductWarehouseID: item.ProductWarehouseId,
|
||||||
|
SubQty: item.SubQty,
|
||||||
|
TotalQty: item.TotalQty,
|
||||||
|
TotalUsed: item.TotalUsed,
|
||||||
|
Price: item.Price,
|
||||||
|
TotalPrice: item.TotalPrice,
|
||||||
|
}
|
||||||
|
if item.Product != nil {
|
||||||
|
dto.Product = toProductBaseDTO(item.Product)
|
||||||
|
}
|
||||||
|
if item.Warehouse != nil {
|
||||||
|
dto.Warehouse = toWarehouseBaseDTO(item.Warehouse)
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
|
||||||
|
result := make([]PurchaseItemDTO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
result[i] = ToPurchaseItemDTO(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
||||||
|
return PurchaseDetailDTO{
|
||||||
|
Id: p.Id,
|
||||||
|
PrNumber: p.PrNumber,
|
||||||
|
Supplier: toSupplierBaseDTO(p.Supplier),
|
||||||
|
CreditTerm: p.CreditTerm,
|
||||||
|
DueDate: p.DueDate,
|
||||||
|
GrandTotal: p.GrandTotal,
|
||||||
|
Notes: p.Notes,
|
||||||
|
Items: ToPurchaseItemDTOs(p.Items),
|
||||||
|
CreatedAt: p.CreatedAt,
|
||||||
|
UpdatedAt: p.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package purchases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseModule struct{}
|
||||||
|
|
||||||
|
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
RegisterRoutes(router, db, validate)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseRepository interface {
|
||||||
|
repository.BaseRepository[entity.Purchase]
|
||||||
|
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||||
|
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
|
||||||
|
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Purchase]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||||
|
return &PurchaseRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
if err := db.Create(purchase).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
item.PurchaseId = purchase.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&items).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
|
||||||
|
var purchase entity.Purchase
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("Supplier").
|
||||||
|
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("id ASC")
|
||||||
|
}).
|
||||||
|
Preload("Items.Product").
|
||||||
|
Preload("Items.Warehouse").
|
||||||
|
Preload("Items.Warehouse.Area").
|
||||||
|
Preload("Items.Warehouse.Location").
|
||||||
|
Preload("Items.ProductWarehouse").
|
||||||
|
First(&purchase, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &purchase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasePricingUpdate struct {
|
||||||
|
ItemID uint64
|
||||||
|
Price float64
|
||||||
|
TotalPrice float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||||
|
ctx context.Context,
|
||||||
|
purchaseID uint64,
|
||||||
|
updates []PurchasePricingUpdate,
|
||||||
|
grandTotal float64,
|
||||||
|
) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return errors.New("pricing updates cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
for _, upd := range updates {
|
||||||
|
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()"),
|
||||||
|
})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&entity.Purchase{}).
|
||||||
|
Where("id = ?", purchaseID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"grand_total": grandTotal,
|
||||||
|
"updated_at": gorm.Expr("NOW()"),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package purchases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
|
||||||
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
group := router.Group("/purchases")
|
||||||
|
|
||||||
|
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseService := service.NewPurchaseService(
|
||||||
|
validate,
|
||||||
|
purchaseRepo,
|
||||||
|
productWarehouseRepo,
|
||||||
|
warehouseRepo,
|
||||||
|
supplierRepo,
|
||||||
|
approvalRepo,
|
||||||
|
)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
PurchaseRoutes(group, userService, purchaseService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PurchaseRoutes(v1 fiber.Router, u sUser.UserService, s service.PurchaseService) {
|
||||||
|
ctrl := controller.NewPurchaseController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/requisitions")
|
||||||
|
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseService interface {
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error)
|
||||||
|
ApproveStaffPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type purchaseService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
PurchaseRepo rPurchase.PurchaseRepository
|
||||||
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
|
SupplierRepo rSupplier.SupplierRepository
|
||||||
|
ApprovalRepo commonRepo.ApprovalRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseService(
|
||||||
|
validate *validator.Validate,
|
||||||
|
purchaseRepo rPurchase.PurchaseRepository,
|
||||||
|
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||||
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
|
supplierRepo rSupplier.SupplierRepository,
|
||||||
|
approvalRepo commonRepo.ApprovalRepository,
|
||||||
|
) PurchaseService {
|
||||||
|
return &purchaseService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
PurchaseRepo: purchaseRepo,
|
||||||
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
SupplierRepo: supplierRepo,
|
||||||
|
ApprovalRepo: approvalRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64Ptr(v uint64) *uint64 {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
supplier, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get supplier: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouse, err := s.WarehouseRepo.GetDetailByID(c.Context(), req.WarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.AreaId != req.AreaID {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided area")
|
||||||
|
}
|
||||||
|
if warehouse.LocationId == nil || *warehouse.LocationId != req.LocationID {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided location")
|
||||||
|
}
|
||||||
|
|
||||||
|
type aggregatedItem struct {
|
||||||
|
productId uint64
|
||||||
|
warehouseId uint64
|
||||||
|
productWarehouseId *uint64
|
||||||
|
subQty float64
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Items) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||||
|
indexMap := make(map[string]int)
|
||||||
|
|
||||||
|
for _, item := range req.Items {
|
||||||
|
var (
|
||||||
|
productId = uint64(item.ProductID)
|
||||||
|
warehouseId = uint64(req.WarehouseID)
|
||||||
|
productWarehouseId *uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.ProductWarehouseID != nil {
|
||||||
|
productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), *item.ProductWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", *item.ProductWarehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get product warehouse %d: %+v", *item.ProductWarehouseID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if productWarehouse.WarehouseId != req.WarehouseID {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse does not match selected warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
productId = uint64(productWarehouse.ProductId)
|
||||||
|
warehouseId = uint64(productWarehouse.WarehouseId)
|
||||||
|
idCopy := uint64(productWarehouse.Id)
|
||||||
|
productWarehouseId = &idCopy
|
||||||
|
} else {
|
||||||
|
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), item.ProductID, req.WarehouseID)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to get product warehouse for product %d and warehouse %d: %+v", item.ProductID, req.WarehouseID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
idCopy := uint64(productWarehouse.Id)
|
||||||
|
productWarehouseId = &idCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%d:%d", productId, warehouseId)
|
||||||
|
if idx, ok := indexMap[key]; ok {
|
||||||
|
aggregated[idx].subQty += item.Quantity
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &aggregatedItem{
|
||||||
|
productId: productId,
|
||||||
|
warehouseId: warehouseId,
|
||||||
|
productWarehouseId: productWarehouseId,
|
||||||
|
subQty: item.Quantity,
|
||||||
|
}
|
||||||
|
aggregated = append(aggregated, entry)
|
||||||
|
indexMap[key] = len(aggregated) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prNumber := fmt.Sprintf("PR-%s-%s", time.Now().Format("20060102"), uuid.NewString()[:8])
|
||||||
|
|
||||||
|
var creditTerm *int
|
||||||
|
var dueDate *time.Time
|
||||||
|
|
||||||
|
if supplier.DueDate > 0 {
|
||||||
|
ct := supplier.DueDate
|
||||||
|
creditTerm = &ct
|
||||||
|
d := time.Now().UTC().AddDate(0, 0, ct)
|
||||||
|
dueDate = &d
|
||||||
|
}
|
||||||
|
|
||||||
|
purchase := &entity.Purchase{
|
||||||
|
PrNumber: prNumber,
|
||||||
|
SupplierId: uint64(req.SupplierID),
|
||||||
|
CreditTerm: creditTerm,
|
||||||
|
DueDate: dueDate,
|
||||||
|
GrandTotal: 0,
|
||||||
|
Notes: req.Notes,
|
||||||
|
CreatedBy: 1, // TODO: replace with authenticated user id once available
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
||||||
|
for _, item := range aggregated {
|
||||||
|
items = append(items, &entity.PurchaseItem{
|
||||||
|
ProductId: item.productId,
|
||||||
|
WarehouseId: item.warehouseId,
|
||||||
|
ProductWarehouseId: item.productWarehouseId,
|
||||||
|
SubQty: item.subQty,
|
||||||
|
TotalQty: item.subQty,
|
||||||
|
TotalUsed: 0,
|
||||||
|
Price: 0,
|
||||||
|
TotalPrice: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Context()
|
||||||
|
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||||
|
if err := purchaseRepoTx.CreateWithItems(ctx, purchase, items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(purchase.CreatedBy)
|
||||||
|
if actorID == 0 {
|
||||||
|
actorID = 1
|
||||||
|
}
|
||||||
|
action := entity.ApprovalActionCreated
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
ctx,
|
||||||
|
utils.ApprovalWorkflowPurchase,
|
||||||
|
uint(purchase.Id),
|
||||||
|
utils.PurchaseStepPengajuan,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if transactionErr != nil {
|
||||||
|
s.Log.Errorf("Failed to create purchase requisition: %+v", transactionErr)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to load created purchase requisition: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Context()
|
||||||
|
|
||||||
|
purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase requisition not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get purchase requisition: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(purchase.Items) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase requisition has no items to approve")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items))
|
||||||
|
for _, item := range req.Items {
|
||||||
|
requestItems[item.PurchaseItemID] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items))
|
||||||
|
var grandTotal float64
|
||||||
|
|
||||||
|
for _, item := range purchase.Items {
|
||||||
|
data, ok := requestItems[item.Id]
|
||||||
|
if !ok {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||||
|
}
|
||||||
|
delete(requestItems, item.Id)
|
||||||
|
|
||||||
|
if data.Price <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for item %d must be greater than 0", item.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPrice := data.TotalPrice
|
||||||
|
if totalPrice == nil {
|
||||||
|
calculated := data.Price * item.TotalQty
|
||||||
|
totalPrice = &calculated
|
||||||
|
}
|
||||||
|
if *totalPrice <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for item %d must be greater than 0", item.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
updates = append(updates, rPurchase.PurchasePricingUpdate{
|
||||||
|
ItemID: item.Id,
|
||||||
|
Price: data.Price,
|
||||||
|
TotalPrice: *totalPrice,
|
||||||
|
})
|
||||||
|
grandTotal += *totalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(requestItems) > 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
action := entity.ApprovalActionApproved
|
||||||
|
actorID := uint(1) // TODO: replace with authenticated user id once available
|
||||||
|
|
||||||
|
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||||
|
if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, updates, grandTotal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
ctx,
|
||||||
|
utils.ApprovalWorkflowPurchase,
|
||||||
|
uint(purchase.Id),
|
||||||
|
utils.PurchaseStepStaffPurchase,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if transactionErr != nil {
|
||||||
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to approve purchase requisition %d: %+v", purchase.Id, transactionErr)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to load purchase requisition after approval: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type PurchaseItemPayload struct {
|
||||||
|
ProductID uint `json:"product_id" validate:"required"`
|
||||||
|
ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePurchaseRequest struct {
|
||||||
|
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||||
|
AreaID uint `json:"area_id" validate:"required"`
|
||||||
|
LocationID uint `json:"location_id" validate:"required"`
|
||||||
|
WarehouseID uint `json:"warehouse_id" validate:"required"`
|
||||||
|
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||||
|
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaffPurchaseApprovalItem struct {
|
||||||
|
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
|
||||||
|
Price float64 `json:"price" validate:"required,gt=0"`
|
||||||
|
TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApproveStaffPurchaseRequest struct {
|
||||||
|
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
||||||
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
||||||
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
||||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||||
|
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||||
|
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
|
||||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
|
||||||
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +29,9 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
|||||||
master.MasterModule{},
|
master.MasterModule{},
|
||||||
constants.ConstantModule{},
|
constants.ConstantModule{},
|
||||||
inventory.InventoryModule{},
|
inventory.InventoryModule{},
|
||||||
production.ProductionModule{},
|
production.ProductionModule{},
|
||||||
approvals.ApprovalModule{},
|
approvals.ApprovalModule{},
|
||||||
|
purchases.PurchaseModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,8 +205,23 @@ const (
|
|||||||
|
|
||||||
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
RecordingStepGradingTelur: "Grading-Telur",
|
RecordingStepGradingTelur: "Grading-Telur",
|
||||||
RecordingStepPengajuan: "Pengajuan",
|
RecordingStepPengajuan: "Pengajuan",
|
||||||
RecordingStepDisetujui: "Disetujui",
|
RecordingStepDisetujui: "Disetujui",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Purchase Approval
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES")
|
||||||
|
PurchaseStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
|
PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
PurchaseStepPengajuan: "Pengajuan",
|
||||||
|
PurchaseStepStaffPurchase: "Staff Purchase",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.Re
|
|||||||
|
|
||||||
result := make([]entity.RecordingBW, 0, len(items))
|
result := make([]entity.RecordingBW, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
totalWeight := item.TotalWeight
|
var totalWeight float64
|
||||||
|
if item.TotalWeight != nil {
|
||||||
|
totalWeight = *item.TotalWeight
|
||||||
|
}
|
||||||
if totalWeight <= 0 {
|
if totalWeight <= 0 {
|
||||||
totalWeight = item.AvgWeight * item.Qty
|
totalWeight = item.AvgWeight * item.Qty
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user