mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
+70
@@ -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).
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user