Merge branch 'dev/teguh' into 'feat/BE/US-159,160/marketing'

[FIX/BE][US#159/TASK#222] :  fixing approval status when updated and delete timestamz on children of marketing table

See merge request mbugroup/lti-api!62
This commit is contained in:
Hafizh A. Y.
2025-11-18 06:54:40 +00:00
37 changed files with 2437 additions and 532 deletions
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error) FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error) LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error) LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
} }
type approvalRepositoryImpl struct { type approvalRepositoryImpl struct {
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
return result, nil return result, nil
} }
func (r *approvalRepositoryImpl) DeleteByTarget(
ctx context.Context,
workflow string,
approvableID uint,
) error {
return r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Delete(&entity.Approval{}).Error
}
@@ -9,13 +9,12 @@ CREATE TABLE IF NOT EXISTS purchase_items (
travel_number_docs VARCHAR, travel_number_docs VARCHAR,
vehicle_number VARCHAR, vehicle_number VARCHAR,
sub_qty NUMERIC(15, 3) NOT NULL, sub_qty NUMERIC(15, 3) NOT NULL,
total_qty NUMERIC(15, 3) DEFAULT 0, total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_used NUMERIC(15, 3) DEFAULT 0, total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
price NUMERIC(15, 3) DEFAULT 0, price NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_price NUMERIC(15, 3) DEFAULT 0, total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT uq_purchase_items_purchase_product_warehouse
updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE (purchase_id, product_id, warehouse_id)
deleted_at TIMESTAMPTZ
); );
DO $$ DO $$
@@ -46,14 +45,10 @@ BEGIN
REFERENCES product_warehouses(id) REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE'; ON DELETE SET NULL ON UPDATE CASCADE';
END IF; END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation END $$;
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_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_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_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_purchase_id ON purchase_items (purchase_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at);
@@ -1,17 +1,19 @@
CREATE TABLE IF NOT EXISTS purchases ( CREATE TABLE IF NOT EXISTS purchases (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
pr_number VARCHAR NOT NULL, pr_number VARCHAR NOT NULL,
po_number VARCHAR, po_number VARCHAR NULL,
po_date TIMESTAMPTZ, po_date TIMESTAMPTZ NULL,
supplier_id BIGINT NOT NULL, supplier_id BIGINT NOT NULL,
credit_term INT, credit_term INT NOT NULL,
due_date TIMESTAMPTZ, due_date TIMESTAMPTZ,
grand_total NUMERIC(15, 3) DEFAULT 0, grand_total NUMERIC(15, 3) NOT NULL,
notes TEXT, notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL created_by BIGINT NOT NULL,
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
); );
DO $$ DO $$
@@ -50,14 +52,6 @@ BEGIN
END IF; END IF;
END $$; 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_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_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_po_date ON purchases (po_date);
@@ -0,0 +1,11 @@
-- Add back timestamp columns to marketing_products table
ALTER TABLE marketing_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
-- Add back timestamp columns to marketing_delivery_products table
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
@@ -0,0 +1,27 @@
-- Drop timestamp columns from marketing_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_products DROP COLUMN deleted_at;
END IF;
END $$;
-- Drop timestamp columns from marketing_delivery_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at;
END IF;
END $$;
+19 -7
View File
@@ -1,18 +1,30 @@
package entities package entities
import ( import (
"database/sql"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
) )
type Expense struct { type Expense struct {
Id uint `gorm:"primaryKey"` Id uint64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` ReferenceNumber *string `gorm:"type:varchar(50)"`
CreatedBy uint `gorm:"not null"` SupplierId *uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"` Category string `gorm:"type:varchar(50);not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` PoNumber string `gorm:"uniqueIndex;not null;type:varchar(50)"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DocumentPath sql.NullString `gorm:"type:json"`
ExpenseDate time.Time `gorm:"type:date;not null"`
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Note *string `gorm:"type:text"`
CreatedBy *uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` // Relations
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
} }
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
Note *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realizations []ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
}
+25
View File
@@ -0,0 +1,25 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ExpenseRealization struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseNonstockId *uint64 `gorm:""`
RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
RealizationDate time.Time `gorm:"type:date;not null"`
Note *string `gorm:"type:text"`
CreatedBy *uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -2,23 +2,18 @@ package entities
import ( import (
"time" "time"
"gorm.io/gorm"
) )
type MarketingDeliveryProduct struct { type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"` MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"` Qty float64 `gorm:"type:numeric(15,3)"`
UnitPrice float64 `gorm:"type:numeric(15,3)"` UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"` TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"` AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"` TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"` DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"` VehicleNumber string `gorm:"type:varchar(50)"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
} }
+8 -17
View File
@@ -1,23 +1,14 @@
package entities package entities
import (
"time"
"gorm.io/gorm"
)
type MarketingProduct struct { type MarketingProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
MarketingId uint `gorm:"not null"` MarketingId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"` ProductWarehouseId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"` UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"` AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"` TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"` TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
+4 -3
View File
@@ -20,7 +20,8 @@ type Purchase struct {
CreatedBy uint64 `gorm:"not null"` CreatedBy uint64 `gorm:"not null"`
// Relations // Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"` Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
} }
+5 -8
View File
@@ -14,14 +14,11 @@ type PurchaseItem struct {
TravelNumber *string TravelNumber *string
TravelNumberDocs *string TravelNumberDocs *string
VehicleNumber *string VehicleNumber *string
SubQty float64 `gorm:"type:numeric(15,3);not null"` SubQty float64 `gorm:"type:numeric(15,3);not null"`
TotalQty float64 `gorm:"type:numeric(15,3);default:0"` TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
TotalUsed 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"` Price float64 `gorm:"type:numeric(15,3);default:0"`
TotalPrice 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 // Relations
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"` Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
+27 -25
View File
@@ -10,44 +10,52 @@ import (
// === DTO Structs === // === DTO Structs ===
type ExpenseBaseDTO struct { type ExpenseBaseDTO struct {
Id uint `json:"id"` Id uint64 `json:"id"`
Name string `json:"name"` PoNumber string `json:"po_number"`
ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
} }
type ExpenseListDTO struct { type ExpenseListDTO struct {
Id uint `json:"id"` Id uint64 `json:"id"`
Name string `json:"name"` ReferenceNumber string `json:"reference_number"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` PoNumber string `json:"po_number"`
CreatedAt time.Time `json:"created_at"` Category string `json:"category"`
UpdatedAt time.Time `json:"updated_at"` ExpenseDate time.Time `json:"expense_date"`
} GrandTotal float64 `json:"grand_total"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
type ExpenseDetailDTO struct { CreatedAt time.Time `json:"created_at"`
ExpenseListDTO UpdatedAt time.Time `json:"updated_at"`
} }
// === Mapper Functions === // === Mapper Functions ===
func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO {
return ExpenseBaseDTO{ return ExpenseBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, PoNumber: e.PoNumber,
ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
} }
} }
func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
var createdUser *userDTO.UserBaseDTO var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser) mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
return ExpenseListDTO{ return ExpenseListDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, ReferenceNumber: *e.ReferenceNumber,
CreatedAt: e.CreatedAt, PoNumber: e.PoNumber,
UpdatedAt: e.UpdatedAt, Category: e.Category,
CreatedUser: createdUser, ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
} }
} }
@@ -58,9 +66,3 @@ func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO {
} }
return result return result
} }
func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO {
return ExpenseDetailDTO{
ExpenseListDTO: ToExpenseListDTO(e),
}
}
@@ -1,13 +1,16 @@
package repository package repository
import ( import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ExpenseRepository interface { type ExpenseRepository interface {
repository.BaseRepository[entity.Expense] repository.BaseRepository[entity.Expense]
IdExists(ctx context.Context, id uint64) (bool, error)
} }
type ExpenseRepositoryImpl struct { type ExpenseRepositoryImpl struct {
@@ -19,3 +22,7 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db),
} }
} }
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
}
@@ -0,0 +1,28 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ExpenseNonstockRepository interface {
repository.BaseRepository[entity.ExpenseNonstock]
IdExists(ctx context.Context, id uint64) (bool, error)
}
type ExpenseNonstockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ExpenseNonstock]
}
func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository {
return &ExpenseNonstockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseNonstock](db),
}
}
func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id))
}
@@ -0,0 +1,28 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ExpenseRealizationRepository interface {
repository.BaseRepository[entity.ExpenseRealization]
IdExists(ctx context.Context, id uint64) (bool, error)
}
type ExpenseRealizationRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ExpenseRealization]
}
func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository {
return &ExpenseRealizationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseRealization](db),
}
}
func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id))
}
@@ -79,8 +79,11 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
createdBy := uint64(1)
createBody := &entity.Expense{ createBody := &entity.Expense{
Name: req.Name, PoNumber: req.PoNumber,
Category: req.Category,
CreatedBy: &createdBy,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -88,7 +91,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
return s.GetOne(c, createBody.Id) return s.GetOne(c, uint(createBody.Id))
} }
func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) {
@@ -98,8 +101,11 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody := make(map[string]any) updateBody := make(map[string]any)
if req.Name != nil { if req.PoNumber != nil {
updateBody["name"] = *req.Name updateBody["po_number"] = *req.PoNumber
}
if req.Category != nil {
updateBody["category"] = *req.Category
} }
if len(updateBody) == 0 { if len(updateBody) == 0 {
@@ -1,11 +1,13 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3"` PoNumber string `json:"po_number" validate:"required,max=50"`
Category string `json:"category" validate:"required,max=50"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"` PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"`
Category *string `json:"category,omitempty" validate:"omitempty,max=50"`
} }
type Query struct { type Query struct {
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -25,6 +26,8 @@ type ProductWarehouseRepository interface {
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) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -155,6 +158,73 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
return nil return nil
} }
func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error {
if len(affected) == 0 {
return nil
}
ids := make([]uint, 0, len(affected))
for id := range affected {
ids = append(ids, id)
}
var emptyIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
Pluck("id", &emptyIDs).Error; err != nil {
return err
}
if len(emptyIDs) == 0 {
return nil
}
if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs).
Update("product_warehouse_id", nil).Error; err != nil {
return err
}
if err := r.DB().WithContext(ctx).
Where("id IN ?", emptyIDs).
Delete(&entity.ProductWarehouse{}).Error; err != nil {
return err
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context,
productID uint,
warehouseID uint,
createdBy uint64,
) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil {
return record.Id, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
entity := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
Quantity: 0,
CreatedBy: uint(createdBy),
}
if entity.CreatedBy == 0 {
entity.CreatedBy = 1
}
if err := r.CreateOne(ctx, entity, nil); err != nil {
return 0, err
}
return entity.Id, nil
}
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -270,7 +270,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
} }
// Ensure delivery product exists; if not, create default
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
mdp := &entity.MarketingDeliveryProduct{ mdp := &entity.MarketingDeliveryProduct{
@@ -291,53 +290,51 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
} }
} else { } else {
// Create new marketing product (use helper)
if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
} }
} }
} }
// 2) Delete missing old products (prevent deletion if deliveries exist)
for _, old := range oldProducts { for _, old := range oldProducts {
if _, ok := reqByPW[old.ProductWarehouseId]; !ok { if _, ok := reqByPW[old.ProductWarehouseId]; !ok {
// Check delivery product for this marketing product
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
} }
if err == nil { if err == nil {
// If delivery exists (delivery_date not nil or qty > 0), prevent deletion
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
} }
// safe to delete delivery product record
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product")
} }
} }
// Delete marketing product
if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product")
} }
} }
} }
} }
if latestApproval != nil {
if latestApproval != nil && latestApproval.StepNumber == 2 {
actorID := uint(1) // todo: ambil dari auth context actorID := uint(1) // todo: ambil dari auth context
resetNote := "" action := entity.ApprovalActionUpdated
action := entity.ApprovalActionApproved
_, err := approvalSvcTx.CreateApproval( _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
utils.ApprovalWorkflowMarketing, utils.ApprovalWorkflowMarketing,
id, id,
utils.MarketingStepPengajuan, approvalutils.ApprovalStep(latestApproval.StepNumber),
&action, &action,
actorID, actorID,
&resetNote) nil)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") if !errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval")
}
} }
} }
@@ -373,7 +370,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
if len(marketing.Products) > 0 { if len(marketing.Products) > 0 {
for _, product := range marketing.Products { for _, product := range marketing.Products {
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("marketing_product_id = ?", product.Id) return db.Where("marketing_product_id = ?", product.Id).Unscoped()
}); err != nil && err != gorm.ErrRecordNotFound { }); err != nil && err != gorm.ErrRecordNotFound {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
} }
@@ -381,7 +378,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
} }
if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("marketing_id = ?", id) return db.Where("marketing_id = ?", id).Unscoped()
}); err != nil && err != gorm.ErrRecordNotFound { }); err != nil && err != gorm.ErrRecordNotFound {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products")
} }
@@ -20,7 +20,7 @@ type CustomerBaseDTO struct {
AccountNumber string `json:"account_number"` AccountNumber string `json:"account_number"`
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
Pic *userDTO.UserBaseDTO `json:"pic"` Pic *userDTO.UserBaseDTO `json:"pic,omitempty"`
} }
type CustomerListDTO struct { type CustomerListDTO struct {
@@ -15,15 +15,20 @@ type KandangBaseDTO struct {
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
Capacity float64 `json:"capacity"` Capacity float64 `json:"capacity"`
Location *locationDTO.LocationBaseDTO `json:"location"` Location locationDTO.LocationBaseDTO `json:"location,omitempty"`
Pic *userDTO.UserBaseDTO `json:"pic"` Pic userDTO.UserBaseDTO `json:"pic,omitempty"`
} }
type KandangListDTO struct { type KandangListDTO struct {
KandangBaseDTO Id uint `json:"id"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` Name string `json:"name"`
CreatedAt time.Time `json:"created_at"` Status string `json:"status"`
UpdatedAt time.Time `json:"updated_at"` Capacity float64 `json:"capacity"`
Location locationDTO.LocationBaseDTO `json:"location"`
Pic userDTO.UserBaseDTO `json:"pic"`
CreatedUser userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type KandangDetailDTO struct { type KandangDetailDTO struct {
@@ -33,16 +38,16 @@ type KandangDetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
var location *locationDTO.LocationBaseDTO var location locationDTO.LocationBaseDTO
if e.Location.Id != 0 { if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location) mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = &mapped location = mapped
} }
var pic *userDTO.UserBaseDTO var pic userDTO.UserBaseDTO
if e.Pic.Id != 0 { if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic) mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = &mapped pic = mapped
} }
return KandangBaseDTO{ return KandangBaseDTO{
@@ -56,17 +61,33 @@ func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
} }
func ToKandangListDTO(e entity.Kandang) KandangListDTO { func ToKandangListDTO(e entity.Kandang) KandangListDTO {
var createdUser *userDTO.UserBaseDTO var location locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
location = mapped
}
var pic userDTO.UserBaseDTO
if e.Pic.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.Pic)
pic = mapped
}
var createdUser userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.CreatedUser) mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
createdUser = &mapped createdUser = mapped
} }
return KandangListDTO{ return KandangListDTO{
KandangBaseDTO: ToKandangBaseDTO(e), Id: e.Id,
CreatedAt: e.CreatedAt, Name: e.Name,
UpdatedAt: e.UpdatedAt, Status: e.Status,
CreatedUser: createdUser, Location: location,
Pic: pic,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
} }
} }
@@ -14,11 +14,14 @@ type LocationBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Address string `json:"address"` Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
} }
type LocationListDTO struct { type LocationListDTO struct {
LocationBaseDTO Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Area *areaDTO.AreaBaseDTO `json:"area"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -52,11 +55,20 @@ func ToLocationListDTO(e entity.Location) LocationListDTO {
createdUser = &mapped createdUser = &mapped
} }
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
return LocationListDTO{ return LocationListDTO{
LocationBaseDTO: ToLocationBaseDTO(e), Id: e.Id,
CreatedUser: createdUser, Name: e.Name,
CreatedAt: e.CreatedAt, Address: e.Address,
UpdatedAt: e.UpdatedAt, Area: area,
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
} }
} }
@@ -12,16 +12,17 @@ import (
// === DTO Structs === // === DTO Structs ===
type NonstockBaseDTO struct { type NonstockBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
UomID uint `json:"uom_id"` Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
} }
type NonstockListDTO struct { type NonstockListDTO struct {
NonstockBaseDTO Id uint `json:"id"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"` Name string `json:"name"`
Uom *uomDTO.UomBaseDTO `json:"uom"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -35,10 +36,22 @@ type NonstockDetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO { func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return NonstockBaseDTO{ return NonstockBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
UomID: e.UomId, Uom: uomRef,
Flags: flags,
} }
} }
@@ -66,13 +79,13 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
} }
return NonstockListDTO{ return NonstockListDTO{
NonstockBaseDTO: ToNonstockBaseDTO(e), Id: e.Id,
CreatedAt: e.CreatedAt, Name: e.Name,
UpdatedAt: e.UpdatedAt, Uom: uomRef,
CreatedUser: createdUser, CreatedAt: e.CreatedAt,
Uom: uomRef, UpdatedAt: e.UpdatedAt,
Suppliers: suppliers, CreatedUser: createdUser,
Flags: flags, Suppliers: suppliers,
} }
} }
@@ -13,8 +13,10 @@ import (
// === DTO Structs === // === DTO Structs ===
type ProductBaseDTO struct { type ProductBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
Flags []string `json:"flags"`
} }
type ProductListDTO struct { type ProductListDTO struct {
@@ -25,10 +27,8 @@ type ProductListDTO struct {
SellingPrice *float64 `json:"selling_price,omitempty"` SellingPrice *float64 `json:"selling_price,omitempty"`
Tax *float64 `json:"tax,omitempty"` Tax *float64 `json:"tax,omitempty"`
ExpiryPeriod *int `json:"expiry_period,omitempty"` ExpiryPeriod *int `json:"expiry_period,omitempty"`
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"` ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"` Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
Flags []string `json:"flags"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -42,9 +42,22 @@ type ProductDetailDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToProductBaseDTO(e entity.Product) ProductBaseDTO { func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
return ProductBaseDTO{ return ProductBaseDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
Flags: flags,
Uom: uomRef,
} }
} }
@@ -55,12 +68,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
createdUser = &mapped createdUser = &mapped
} }
var uomRef *uomDTO.UomBaseDTO
if e.Uom.Id != 0 {
mapped := uomDTO.ToUomBaseDTO(e.Uom)
uomRef = &mapped
}
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 { if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory) mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
@@ -72,11 +79,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s) suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
} }
flags := make([]string, len(e.Flags))
for i, f := range e.Flags {
flags[i] = f.Name
}
return ProductListDTO{ return ProductListDTO{
Brand: e.Brand, Brand: e.Brand,
Sku: e.Sku, Sku: e.Sku,
@@ -88,10 +90,8 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser, CreatedUser: createdUser,
Uom: uomRef,
ProductCategory: categoryRef, ProductCategory: categoryRef,
Suppliers: suppliers, Suppliers: suppliers,
Flags: flags,
} }
} }
@@ -16,6 +16,7 @@ type ProductRepository interface {
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CategoryExists(ctx context.Context, categoryID uint) (bool, error) CategoryExists(ctx context.Context, categoryID uint) (bool, error)
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error) GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
@@ -90,6 +91,17 @@ func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierI
return suppliers, nil return suppliers, nil
} }
func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.ProductSupplier{}).
Where("product_id = ? AND supplier_id = ?", productID, supplierID).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error { func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
db := tx db := tx
if db == nil { if db == nil {
@@ -16,16 +16,21 @@ type WarehouseBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Area *areaDTO.AreaBaseDTO `json:"area"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location"` Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"` Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
} }
type WarehouseListDTO struct { type WarehouseListDTO struct {
WarehouseBaseDTO Id uint `json:"id"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"` Name string `json:"name"`
CreatedAt time.Time `json:"created_at"` Type string `json:"type"`
UpdatedAt time.Time `json:"updated_at"` Area *areaDTO.AreaBaseDTO `json:"area"`
Location *locationDTO.LocationBaseDTO `json:"location"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type WarehouseDetailDTO struct { type WarehouseDetailDTO struct {
@@ -70,11 +75,34 @@ func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO {
createdUser = &mapped createdUser = &mapped
} }
var area *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
area = &mapped
}
var location *locationDTO.LocationBaseDTO
if e.Location != nil && e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(*e.Location)
location = &mapped
}
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang != nil && e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang)
kandang = &mapped
}
return WarehouseListDTO{ return WarehouseListDTO{
WarehouseBaseDTO: ToWarehouseBaseDTO(e), Id: e.Id,
CreatedAt: e.CreatedAt, Name: e.Name,
UpdatedAt: e.UpdatedAt, Type: e.Type,
CreatedUser: createdUser, Area: area,
Location: location,
Kandang: kandang,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
} }
} }
@@ -1,7 +1,10 @@
package controller package controller
import ( import (
"fmt"
"math"
"strconv" "strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
@@ -19,6 +22,74 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController {
return &PurchaseController{service: s} return &PurchaseController{service: s}
} }
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
query := &validation.PurchaseQuery{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search")),
PrNumber: strings.TrimSpace(c.Query("pr_number")),
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")),
}
if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 {
query.SupplierID = uint(supplierID)
}
if status := strings.TrimSpace(c.Query("status")); status != "" {
query.Status = strings.ToUpper(status)
}
results, total, err := ctrl.service.GetAll(c, query)
if err != nil {
return err
}
limit := query.Limit
if limit <= 0 {
limit = 10
}
page := query.Page
if page <= 0 {
page = 1
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase fetched successfully",
Meta: response.Meta{
Page: page,
Limit: limit,
TotalPages: int64(math.Ceil(float64(total) / float64(limit))),
TotalResults: total,
},
Data: dto.ToPurchaseListDTOs(results),
})
}
func (ctrl *PurchaseController) GetOne(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 id")
}
result, err := ctrl.service.GetOne(c, id)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase fetched successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error { func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.CreatePurchaseRequest) req := new(validation.CreatePurchaseRequest)
@@ -35,7 +106,7 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusCreated, Code: fiber.StatusCreated,
Status: "success", Status: "success",
Message: "Purchase requisition created successfully", Message: "Purchase created successfully",
Data: dto.ToPurchaseDetailDTO(*result), Data: dto.ToPurchaseDetailDTO(*result),
}) })
} }
@@ -44,12 +115,12 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
req := new(validation.ApproveStaffPurchaseRequest) req := new(validation.ApproveStaffPurchaseRequest)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
} }
result, err := ctrl.service.ApproveStaffPurchase(c, id, req) result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
@@ -65,3 +136,102 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
Data: dto.ToPurchaseDetailDTO(*result), Data: dto.ToPurchaseDetailDTO(*result),
}) })
} }
func (ctrl *PurchaseController) ApproveManagerPurchase(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 id")
}
req := new(validation.ApproveManagerPurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ApproveManagerPurchase(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Manager purchase approval recorded successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) ReceiveProducts(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 id")
}
req := new(validation.ReceivePurchaseRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.ReceiveProducts(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase receiving recorded successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) DeleteItems(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 id")
}
req := new(validation.DeletePurchaseItemsRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := ctrl.service.DeleteItems(c, id, req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase items deleted successfully",
Data: dto.ToPurchaseDetailDTO(*result),
})
}
func (ctrl *PurchaseController) DeletePurchase(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 id")
}
if err := ctrl.service.DeletePurchase(c, id); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Purchase deleted successfully",
Data: nil,
})
}
+123 -105
View File
@@ -4,129 +4,94 @@ import (
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
) )
type SupplierBaseDTO struct { type PurchaseListItemDTO struct {
Id uint `json:"id"` Id uint64 `json:"id"`
Name string `json:"name"` PrNumber string `json:"pr_number"`
Alias string `json:"alias"` PoNumber *string `json:"po_number"`
Type string `json:"type"` Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
Category string `json:"category"` CreditTerm *int `json:"credit_term"`
} DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
type AreaBaseDTO struct { GrandTotal float64 `json:"grand_total"`
Id uint `json:"id"` Notes *string `json:"notes"`
Name string `json:"name"` CreatedAt time.Time `json:"created_at"`
} UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
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 { type PurchaseDetailDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
PrNumber string `json:"pr_number"` PrNumber string `json:"pr_number"`
Supplier *SupplierBaseDTO `json:"supplier,omitempty"` PoNumber *string `json:"po_number"`
CreditTerm *int `json:"credit_term,omitempty"` Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
DueDate *time.Time `json:"due_date,omitempty"` CreditTerm *int `json:"credit_term"`
GrandTotal float64 `json:"grand_total"` DueDate *time.Time `json:"due_date"`
Notes *string `json:"notes,omitempty"` PoDate *time.Time `json:"po_date"`
Items []PurchaseItemDTO `json:"items"` GrandTotal float64 `json:"grand_total"`
CreatedAt time.Time `json:"created_at"` Notes *string `json:"notes"`
UpdatedAt time.Time `json:"updated_at"` Items []PurchaseItemDTO `json:"items"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
} }
func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO { type PurchaseItemDTO struct {
if s.Id == 0 { Id uint64 `json:"id"`
return nil ProductID uint64 `json:"product_id"`
} Product *productDTO.ProductBaseDTO `json:"product"`
return &SupplierBaseDTO{ WarehouseID uint64 `json:"warehouse_id"`
Id: s.Id, Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse"`
Name: s.Name, ProductWarehouseID *uint64 `json:"product_warehouse_id"`
Alias: s.Alias, SubQty float64 `json:"sub_qty"`
Type: s.Type, TotalQty float64 `json:"total_qty"`
Category: s.Category, TotalUsed float64 `json:"total_used"`
} Price float64 `json:"price"`
} TotalPrice float64 `json:"total_price"`
ReceivedDate *time.Time `json:"received_date"`
func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO { TravelNumber *string `json:"travel_number"`
if w == nil || w.Id == 0 { TravelDocumentPath *string `json:"travel_document_path"`
return nil VehicleNumber *string `json:"vehicle_number"`
}
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 { func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
dto := PurchaseItemDTO{ dto := PurchaseItemDTO{
Id: item.Id, Id: item.Id,
ProductID: item.ProductId,
ProductWarehouseID: item.ProductWarehouseId, ProductWarehouseID: item.ProductWarehouseId,
WarehouseID: item.WarehouseId,
SubQty: item.SubQty, SubQty: item.SubQty,
TotalQty: item.TotalQty, TotalQty: item.TotalQty,
TotalUsed: item.TotalUsed, TotalUsed: item.TotalUsed,
Price: item.Price, Price: item.Price,
TotalPrice: item.TotalPrice, TotalPrice: item.TotalPrice,
ReceivedDate: item.ReceivedDate,
TravelNumber: item.TravelNumber,
TravelDocumentPath: item.TravelNumberDocs,
VehicleNumber: item.VehicleNumber,
} }
if item.Product != nil { if item.Product != nil && item.Product.Id != 0 {
dto.Product = toProductBaseDTO(item.Product) summary := productDTO.ToProductBaseDTO(*item.Product)
dto.Product = &summary
} }
if item.Warehouse != nil { if item.Warehouse != nil && item.Warehouse.Id != 0 {
dto.Warehouse = toWarehouseBaseDTO(item.Warehouse) summary := warehouseDTO.ToWarehouseBaseDTO(*item.Warehouse)
if item.Warehouse.Area.Id != 0 {
areaSummary := areaDTO.ToAreaBaseDTO(item.Warehouse.Area)
summary.Area = &areaSummary
}
if item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 {
locationSummary := locationDTO.ToLocationBaseDTO(*item.Warehouse.Location)
summary.Location = &locationSummary
}
dto.Warehouse = &summary
} }
return dto return dto
} }
@@ -140,16 +105,69 @@ func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
} }
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO { func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
return PurchaseDetailDTO{ dto := PurchaseDetailDTO{
Id: p.Id, Id: p.Id,
PrNumber: p.PrNumber, PrNumber: p.PrNumber,
Supplier: toSupplierBaseDTO(p.Supplier), PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm, CreditTerm: p.CreditTerm,
DueDate: p.DueDate, DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal, GrandTotal: p.GrandTotal,
Notes: p.Notes, Notes: p.Notes,
Items: ToPurchaseItemDTOs(p.Items), Items: ToPurchaseItemDTOs(p.Items),
CreatedAt: p.CreatedAt, CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt, UpdatedAt: p.UpdatedAt,
} }
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO {
dto := PurchaseListItemDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}
if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func mapSupplier(s entity.Supplier) *supplierDTO.SupplierBaseDTO {
if s.Id == 0 {
return nil
}
summary := supplierDTO.ToSupplierBaseDTO(s)
return &summary
}
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO {
if len(items) == 0 {
return nil
}
result := make([]PurchaseListItemDTO, len(items))
for i, item := range items {
result[i] = ToPurchaseListDTO(item)
}
return result
}
func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalBaseDTO {
if p.LatestApproval == nil || p.LatestApproval.Id == 0 {
return nil
}
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
return &mapped
} }
+43 -1
View File
@@ -1,13 +1,55 @@
package purchases package purchases
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "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"
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/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"
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" "gorm.io/gorm"
) )
type PurchaseModule struct{} type PurchaseModule struct{}
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
RegisterRoutes(router, db, validate) purchaseRepo := rPurchase.NewPurchaseRepository(db)
productRepo := rProduct.NewProductRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(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))
}
expenseBridge := service.NewNoopPurchaseExpenseBridge()
purchaseService := service.NewPurchaseService(
validate,
purchaseRepo,
productRepo,
warehouseRepo,
supplierRepo,
productWarehouseRepo,
approvalRepo,
approvalService,
expenseBridge,
)
userRepo := rUser.NewUserRepository(db)
userService := sUser.NewUserService(userRepo, validate)
Routes(router, purchaseService, userService)
} }
@@ -3,17 +3,31 @@ package repositories
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type PurchaseRepository interface { type PurchaseRepository interface {
repository.BaseRepository[entity.Purchase] repository.BaseRepository[entity.Purchase]
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
WithListRelations() func(*gorm.DB) *gorm.DB
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
} }
type PurchaseRepositoryImpl struct { type PurchaseRepositoryImpl struct {
@@ -26,6 +40,16 @@ func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
} }
} }
type PurchaseListFilter struct {
SupplierID uint
Search string
PrNumber string
CreatedFrom *time.Time
CreatedTo *time.Time
Status *entity.ApprovalAction
CompletedOnly bool
}
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error { func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
db := r.DB().WithContext(ctx) db := r.DB().WithContext(ctx)
@@ -47,9 +71,47 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
return nil return nil
} }
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
if len(items) == 0 {
return nil
}
for _, item := range items {
if item == nil {
continue
}
item.PurchaseId = purchaseID
}
return r.DB().WithContext(ctx).Create(&items).Error
}
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) { func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
var purchase entity.Purchase var purchase entity.Purchase
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Scopes(r.withDetailRelations).
First(&purchase, id).Error
if err != nil {
return nil, err
}
return &purchase, nil
}
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withListRelations(db)
return r.applyListFilters(db, filter)
})
}
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withListRelations(db)
}
}
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Supplier"). Preload("Supplier").
Preload("Items", func(db *gorm.DB) *gorm.DB { Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC") return db.Order("id ASC")
@@ -58,18 +120,34 @@ func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id ui
Preload("Items.Warehouse"). Preload("Items.Warehouse").
Preload("Items.Warehouse.Area"). Preload("Items.Warehouse.Area").
Preload("Items.Warehouse.Location"). Preload("Items.Warehouse.Location").
Preload("Items.ProductWarehouse"). Preload("Items.ProductWarehouse")
First(&purchase, id).Error }
if err != nil {
return nil, err func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDetailRelations(db)
} }
return &purchase, nil
} }
type PurchasePricingUpdate struct { type PurchasePricingUpdate struct {
ItemID uint64 ItemID uint64
ProductID *uint64
Price float64 Price float64
TotalPrice float64 TotalPrice float64
Quantity *float64
TotalQty *float64
}
type PurchaseReceivingUpdate struct {
ItemID uint64
ReceivedDate *time.Time
TravelNumber *string
TravelDocumentPath *string
VehicleNumber *string
ReceivedQty *float64
WarehouseID *uint
ProductWarehouseID *uint
ClearProductWarehouse bool
} }
func (r *PurchaseRepositoryImpl) UpdatePricing( func (r *PurchaseRepositoryImpl) UpdatePricing(
@@ -85,13 +163,23 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
db := r.DB().WithContext(ctx) db := r.DB().WithContext(ctx)
for _, upd := range updates { for _, upd := range updates {
data := map[string]interface{}{
"price": upd.Price,
"total_price": upd.TotalPrice,
}
if upd.ProductID != nil {
data["product_id"] = *upd.ProductID
}
if upd.Quantity != nil {
data["sub_qty"] = *upd.Quantity
}
if upd.TotalQty != nil {
data["total_qty"] = *upd.TotalQty
}
result := db.Model(&entity.PurchaseItem{}). result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID). Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(map[string]interface{}{ Updates(data)
"price": upd.Price,
"total_price": upd.TotalPrice,
"updated_at": gorm.Expr("NOW()"),
})
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
@@ -111,3 +199,225 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
return nil return nil
} }
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
ctx context.Context,
purchaseID uint64,
updates []PurchaseReceivingUpdate,
) error {
if len(updates) == 0 {
return errors.New("receiving updates cannot be empty")
}
db := r.DB().WithContext(ctx)
for _, upd := range updates {
data := map[string]interface{}{}
if upd.ReceivedDate != nil {
data["received_date"] = upd.ReceivedDate
}
if upd.TravelNumber != nil {
data["travel_number"] = upd.TravelNumber
}
if upd.TravelDocumentPath != nil {
data["travel_number_docs"] = upd.TravelDocumentPath
}
if upd.VehicleNumber != nil {
data["vehicle_number"] = upd.VehicleNumber
}
if upd.ReceivedQty != nil {
data["total_qty"] = upd.ReceivedQty
}
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
data["warehouse_id"] = upd.WarehouseID
}
if upd.ProductWarehouseID != nil {
data["product_warehouse_id"] = *upd.ProductWarehouseID
} else if upd.ClearProductWarehouse {
data["product_warehouse_id"] = gorm.Expr("NULL")
}
if len(data) == 0 {
continue
}
result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(data)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
}
return nil
}
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
ctx context.Context,
purchaseID uint64,
grandTotal float64,
) error {
return r.DB().WithContext(ctx).
Model(&entity.Purchase{}).
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
if len(itemIDs) == 0 {
return errors.New("itemIDs cannot be empty")
}
return r.DB().WithContext(ctx).
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
Delete(&entity.PurchaseItem{}).Error
}
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var values []string
err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck(column, &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
exists, err := r.numberExists(ctx, db, column, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", column)
}
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s = ?", column), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Supplier")
}
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
if filter == nil {
return db
}
if filter.SupplierID > 0 {
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
}
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
like := "%" + search + "%"
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
}
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
}
if filter.CreatedFrom != nil {
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
}
if filter.CreatedTo != nil {
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
}
if filter.CompletedOnly {
step := uint16(utils.PurchaseStepCompleted)
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
} else if filter.Status != nil {
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
}
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
}
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
latestSub := r.DB().
Model(&entity.Approval{}).
Select("approvable_id, MAX(action_at) AS latest_action_at").
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
Group("approvable_id")
db = db.
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
Joins(
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
utils.ApprovalWorkflowPurchase.String(),
).
Where("approvals.action = ?", string(action))
if minStep != nil {
db = db.Where("approvals.step_number >= ?", *minStep)
}
return db
}
+13 -46
View File
@@ -1,59 +1,26 @@
package purchases package purchases
import ( import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "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" middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
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" 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" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
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) { func Routes(router fiber.Router, purchaseService service.PurchaseService, userService user.UserService) {
group := router.Group("/purchases") ctrl := controller.NewPurchaseController(purchaseService)
purchaseRepo := rPurchase.NewPurchaseRepository(db) route := router.Group("/purchases")
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) route.Use(middleware.Auth(userService))
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.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase)
route.Post("/:id/receipts", ctrl.ReceiveProducts)
route.Delete("/:id", ctrl.DeletePurchase)
route.Delete("/:id/items", ctrl.DeleteItems)
} }
@@ -0,0 +1,43 @@
package service
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
type PurchaseExpenseBridge interface {
OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error
OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error
}
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
type ExpenseReceivingPayload struct {
PurchaseItemID uint64
ProductID uint64
WarehouseID uint64
ReceivedQty float64
ReceivedDate *time.Time
}
// noopPurchaseExpenseBridge is the default implementation until the expense module is ready.
type noopPurchaseExpenseBridge struct{}
func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
return &noopPurchaseExpenseBridge{}
}
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error {
return nil
}
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error {
return nil
}
File diff suppressed because it is too large Load Diff
@@ -1,27 +1,63 @@
package validation package validation
type PurchaseItemPayload struct { type PurchaseItemPayload struct {
ProductID uint `json:"product_id" validate:"required"` WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"` ProductID uint `json:"product_id" validate:"required,gt=0"`
Quantity float64 `json:"quantity" validate:"required,gt=0"` Quantity float64 `json:"qty" validate:"required,gt=0"`
} }
type CreatePurchaseRequest struct { type CreatePurchaseRequest struct {
SupplierID uint `json:"supplier_id" validate:"required"` SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
AreaID uint `json:"area_id" validate:"required"` CreditTerm int `json:"credit_term" validate:"required,gte=0"`
LocationID uint `json:"location_id" validate:"required"` Notes *string `json:"notes" validate:"omitempty,max=500"`
WarehouseID uint `json:"warehouse_id" validate:"required"` Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes" validate:"omitempty,max=500"`
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
} }
type StaffPurchaseApprovalItem struct { type StaffPurchaseApprovalItem struct {
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"` PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"` // For new items (no purchase_item_id), product_id is required.
TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"` ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
} }
type ApproveStaffPurchaseRequest struct { type ApproveStaffPurchaseRequest struct {
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"` Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type ApproveManagerPurchaseRequest struct {
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type ReceivePurchaseItemRequest struct {
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"`
}
type ReceivePurchaseRequest struct {
Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type DeletePurchaseItemsRequest struct {
ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"`
}
type PurchaseQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
Search string `query:"search" validate:"omitempty,max=100"`
PrNumber string `query:"pr_number" validate:"omitempty,max=50"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"`
}
+4 -2
View File
@@ -10,6 +10,7 @@ import (
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" 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"
expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing" marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing"
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
@@ -17,7 +18,6 @@ import (
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso" ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -32,12 +32,14 @@ func Routes(app *fiber.App, db *gorm.DB) {
master.MasterModule{}, master.MasterModule{},
constants.ConstantModule{}, constants.ConstantModule{},
inventory.InventoryModule{}, inventory.InventoryModule{},
purchases.PurchaseModule{},
production.ProductionModule{}, production.ProductionModule{},
approvals.ApprovalModule{}, approvals.ApprovalModule{},
purchases.PurchaseModule{}, purchases.PurchaseModule{},
marketing.MarketingModule{}, marketing.MarketingModule{},
ssoModule.Module{}, ssoModule.Module{},
expenses.ExpenseModule{}, expenses.ExpenseModule{},
ssoModule.Module{},
// MODULE REGISTRY // MODULE REGISTRY
} }
+27
View File
@@ -217,11 +217,21 @@ const (
ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES") ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES")
PurchaseStepPengajuan approvalutils.ApprovalStep = 1 PurchaseStepPengajuan approvalutils.ApprovalStep = 1
PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2 PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2
PurchaseStepManager approvalutils.ApprovalStep = 3
PurchaseStepReceiving approvalutils.ApprovalStep = 4
PurchaseStepCompleted approvalutils.ApprovalStep = 5
PurchasePRNumberPrefix = "PR-LTI-"
PurchasePONumberPrefix = "PO-LTI-"
PurchaseNumberPadding = 4
) )
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
PurchaseStepPengajuan: "Pengajuan", PurchaseStepPengajuan: "Pengajuan",
PurchaseStepStaffPurchase: "Staff Purchase", PurchaseStepStaffPurchase: "Staff Purchase",
PurchaseStepManager: "Manager Purchase",
PurchaseStepReceiving: "Penerimaan Produk",
PurchaseStepCompleted: "Selesai",
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -241,6 +251,23 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{
MarketingDeliveryOrder: "Delivery Order", MarketingDeliveryOrder: "Delivery Order",
} }
// -------------------------------------------------------------------
// Expense Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES")
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
ExpenseStepManager approvalutils.ApprovalStep = 2
ExpenseStepFinance approvalutils.ApprovalStep = 3
)
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
ExpenseStepPengajuan: "Pengajuan",
ExpenseStepManager: "Manager",
ExpenseStepFinance: "Finance",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------