mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/teguh' into 'feat/BE/Sprint-6'
[FEAT/BE][277] : expense adjustment with new ERD and mockup See merge request mbugroup/lti-api!73
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
CREATE TABLE expenses (
|
CREATE TABLE expenses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
supplier_id BIGINT NULL,
|
supplier_id BIGINT NOT NULL,
|
||||||
category VARCHAR(50) NOT NULL CHECK (
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
category IN ('BOP', 'NON-BOP')
|
category IN ('BOP', 'NON-BOP')
|
||||||
),
|
),
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
-- ============================
|
||||||
|
-- EXPENSES
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date;
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_REALIZATIONS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_qty TO qty;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
RENAME COLUMN realization_unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
DROP COLUMN IF EXISTS realization_date;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_realizations
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
|
|
||||||
|
-- ============================
|
||||||
|
-- EXPENSE_NONSTOCKS
|
||||||
|
-- ============================
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by;
|
||||||
|
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP Table IF EXISTS project_budgets;
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE project_budgets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL,
|
||||||
|
nonstock_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
price NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tambahkan Foreign Key ke project_flocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_nonstock_id
|
||||||
|
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
-- Index
|
||||||
|
CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id);
|
||||||
@@ -13,18 +13,16 @@ type Expense struct {
|
|||||||
SupplierId uint64 `gorm:""`
|
SupplierId uint64 `gorm:""`
|
||||||
Category string `gorm:"type:varchar(50);not null"`
|
Category string `gorm:"type:varchar(50);not null"`
|
||||||
PoNumber string `gorm:"type:varchar(50)"`
|
PoNumber string `gorm:"type:varchar(50)"`
|
||||||
DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
|
DocumentPath sql.NullString `gorm:"type:json"`
|
||||||
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
|
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||||
ExpenseDate time.Time `gorm:"type:date;not null"`
|
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
Note string `gorm:"type:text"`
|
|
||||||
CreatedBy uint64 `gorm:""`
|
CreatedBy uint64 `gorm:""`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
// Relations
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
type ExpenseNonstock struct {
|
import (
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
"time"
|
||||||
ExpenseId *uint64 `gorm:""`
|
)
|
||||||
ProjectFlockKandangId *uint64 `gorm:""`
|
|
||||||
KandangId *uint64 `gorm:""`
|
type ExpenseNonstock struct {
|
||||||
NonstockId *uint64 `gorm:""`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
ExpenseId *uint64 `gorm:""`
|
||||||
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
ProjectFlockKandangId *uint64 `gorm:""`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
KandangId *uint64 `gorm:""`
|
||||||
Note string `gorm:"type:text"`
|
NonstockId *uint64 `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
|
||||||
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
|
|
||||||
// Relations
|
|
||||||
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseRealization struct {
|
type ExpenseRealization struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ExpenseNonstockId *uint64 `gorm:""`
|
ExpenseNonstockId *uint64 `gorm:""`
|
||||||
RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
|
Qty float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Price float64 `gorm:"type:numeric(15,3);not null;"`
|
||||||
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
|
Notes string `gorm:"type:text;"`
|
||||||
RealizationDate time.Time `gorm:"type:date;not null"`
|
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
|
||||||
Note *string `gorm:"type:text"`
|
|
||||||
CreatedBy *uint64 `gorm:""`
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectBudget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
||||||
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
||||||
|
}
|
||||||
@@ -96,30 +96,30 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
costPerKandangJSON := c.FormValue("cost_per_kandangs")
|
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
|
||||||
if costPerKandangJSON != "" {
|
if expenseNonstocksJSON != "" {
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil {
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil {
|
||||||
|
|
||||||
var singleCostPerKandang validation.CostPerKandang
|
var singleExpenseNonstock validation.ExpenseNonstock
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if singleCostPerKandang.KandangID == 0 {
|
if singleExpenseNonstock.KandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
|
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
|
req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock}
|
||||||
} else {
|
} else {
|
||||||
for i, costPerKandang := range req.CostPerKandangs {
|
for i, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
if costPerKandang.KandangID == 0 {
|
if expenseNonstock.KandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required")
|
return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ExpenseService.CreateOne(c, req)
|
result, err := u.ExpenseService.CreateOne(c, req)
|
||||||
@@ -155,20 +155,32 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
req.TransactionDate = &transactionDate
|
req.TransactionDate = &transactionDate
|
||||||
}
|
}
|
||||||
|
|
||||||
costPerKandangJSON := c.FormValue("cost_per_kandang")
|
categoryVal := c.FormValue("category")
|
||||||
if costPerKandangJSON != "" {
|
req.Category = &categoryVal
|
||||||
var costPerKandang []validation.CostPerKandang
|
|
||||||
if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil {
|
supplierIDVal := c.FormValue("supplier_id")
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
|
if supplierIDVal != "" {
|
||||||
|
supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
|
||||||
|
}
|
||||||
|
req.SupplierID = &supplierID
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
|
||||||
|
if expenseNonstocksJSON != "" {
|
||||||
|
var expenseNonstocks []validation.ExpenseNonstock
|
||||||
|
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, costPerKandang := range costPerKandang {
|
for i, expenseNonstock := range expenseNonstocks {
|
||||||
if costPerKandang.KandangID == 0 {
|
if expenseNonstock.KandangID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.CostPerKandang = &costPerKandang
|
req.ExpenseNonstocks = &expenseNonstocks
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
|
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type ExpenseRelationDTO struct {
|
type ExpenseRelationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
PoNumber string `json:"po_number"`
|
PoNumber string `json:"po_number"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseBaseDTO struct {
|
type ExpenseBaseDTO struct {
|
||||||
@@ -28,8 +27,7 @@ type ExpenseBaseDTO struct {
|
|||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
|
||||||
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
RealizationDate *time.Time `json:"realization_date,omitempty"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,21 +53,26 @@ type ExpenseDetailDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseNonstockDTO struct {
|
type ExpenseNonstockDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Qty float64 `json:"qty"`
|
ExpenseId *uint64 `json:"expense_id,omitempty"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
KandangId *uint64 `json:"kandang_id,omitempty"`
|
||||||
Note *string `json:"note,omitempty"`
|
NonstockId *uint64 `json:"nonstock_id,omitempty"`
|
||||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
Qty float64 `json:"qty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationDTO struct {
|
type ExpenseRealizationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Qty float64 `json:"qty"`
|
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
|
||||||
UnitPrice float64 `json:"unit_price"`
|
Qty float64 `json:"qty"`
|
||||||
TotalPrice float64 `json:"total_price"`
|
Price float64 `json:"price"`
|
||||||
Note *string `json:"note,omitempty"`
|
Notes string `json:"notes"`
|
||||||
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangGroupDTO struct {
|
type KandangGroupDTO struct {
|
||||||
@@ -89,10 +92,9 @@ type DocumentDTO struct {
|
|||||||
|
|
||||||
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
|
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
|
||||||
return ExpenseRelationDTO{
|
return ExpenseRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
PoNumber: e.PoNumber,
|
PoNumber: e.PoNumber,
|
||||||
ExpenseDate: e.ExpenseDate,
|
TransactionDate: e.TransactionDate,
|
||||||
GrandTotal: e.GrandTotal,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +126,7 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
|
|||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
Supplier: supplier,
|
Supplier: supplier,
|
||||||
RealizationDate: realizationDate,
|
RealizationDate: realizationDate,
|
||||||
ExpenseDate: e.ExpenseDate,
|
TransactionDate: e.TransactionDate,
|
||||||
GrandTotal: e.GrandTotal,
|
|
||||||
Location: location,
|
Location: location,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +193,9 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
|
|
||||||
for _, ns := range e.Nonstocks {
|
for _, ns := range e.Nonstocks {
|
||||||
pengajuanDTO := ToExpenseNonstockDTO(ns)
|
pengajuanDTO := ToExpenseNonstockDTO(ns)
|
||||||
|
|
||||||
pengajuans = append(pengajuans, pengajuanDTO)
|
pengajuans = append(pengajuans, pengajuanDTO)
|
||||||
|
|
||||||
if ns.Realization != nil && ns.Realization.Id != 0 {
|
if ns.Realization != nil {
|
||||||
var nonstock *nonstockDTO.NonstockRelationDTO
|
var nonstock *nonstockDTO.NonstockRelationDTO
|
||||||
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||||
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
|
||||||
@@ -203,12 +203,13 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
realisasiDTO := ExpenseRealizationDTO{
|
realisasiDTO := ExpenseRealizationDTO{
|
||||||
Id: ns.Realization.Id,
|
Id: ns.Realization.Id,
|
||||||
Qty: ns.Realization.RealizationQty,
|
ExpenseNonstockId: ns.Realization.ExpenseNonstockId,
|
||||||
UnitPrice: ns.Realization.RealizationUnitPrice,
|
Qty: ns.Realization.Qty,
|
||||||
TotalPrice: ns.Realization.RealizationTotalPrice,
|
Price: ns.Realization.Price,
|
||||||
Note: ns.Realization.Note,
|
Notes: ns.Realization.Notes,
|
||||||
Nonstock: nonstock,
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: ns.Realization.CreatedAt,
|
||||||
}
|
}
|
||||||
realisasi = append(realisasi, realisasiDTO)
|
realisasi = append(realisasi, realisasiDTO)
|
||||||
}
|
}
|
||||||
@@ -217,12 +218,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
|
|
||||||
var totalPengajuan float64
|
var totalPengajuan float64
|
||||||
for _, p := range pengajuans {
|
for _, p := range pengajuans {
|
||||||
totalPengajuan += p.TotalPrice
|
totalPengajuan += p.Qty * p.Price
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalRealisasi float64
|
var totalRealisasi float64
|
||||||
for _, r := range realisasi {
|
for _, r := range realisasi {
|
||||||
totalRealisasi += r.TotalPrice
|
totalRealisasi += r.Qty * r.Price
|
||||||
}
|
}
|
||||||
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
|
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
|
||||||
|
|
||||||
@@ -248,12 +249,16 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ExpenseNonstockDTO{
|
return ExpenseNonstockDTO{
|
||||||
Id: ns.Id,
|
Id: ns.Id,
|
||||||
Qty: ns.Qty,
|
ExpenseId: ns.ExpenseId,
|
||||||
UnitPrice: ns.UnitPrice,
|
ProjectFlockKandangId: ns.ProjectFlockKandangId,
|
||||||
TotalPrice: ns.TotalPrice,
|
KandangId: ns.KandangId,
|
||||||
Note: &ns.Note,
|
NonstockId: ns.NonstockId,
|
||||||
Nonstock: nonstock,
|
Qty: ns.Qty,
|
||||||
|
Price: ns.Price,
|
||||||
|
Notes: ns.Notes,
|
||||||
|
Nonstock: nonstock,
|
||||||
|
CreatedAt: ns.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +269,13 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
|||||||
var kandangId uint64
|
var kandangId uint64
|
||||||
var kandangName string
|
var kandangName string
|
||||||
|
|
||||||
for _, ns := range nonstocks {
|
if p.KandangId != nil {
|
||||||
if ns.Id == p.Id && ns.Kandang != nil {
|
kandangId = *p.KandangId
|
||||||
kandangId = uint64(ns.Kandang.Id)
|
for _, ns := range nonstocks {
|
||||||
kandangName = ns.Kandang.Name
|
if ns.Id == p.Id && ns.Kandang != nil {
|
||||||
break
|
kandangName = ns.Kandang.Name
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
type ExpenseRepository interface {
|
type ExpenseRepository interface {
|
||||||
repository.BaseRepository[entity.Expense]
|
repository.BaseRepository[entity.Expense]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetNextSequence(ctx context.Context) (int, error)
|
GetNextSequence(ctx context.Context) (int, error)
|
||||||
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
|
return repository.Exists[entity.Expense](ctx, r.DB(), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
|
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"time"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
@@ -148,8 +147,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
for _, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
nonstockId := uint(costItem.NonstockID)
|
nonstockId := uint(costItem.NonstockID)
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
@@ -189,21 +188,13 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||||
}
|
}
|
||||||
|
|
||||||
var grandTotal float64
|
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
|
||||||
grandTotal += costItem.TotalCost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createdBy := uint64(1) //todo get from auth
|
createdBy := uint64(1) //todo get from auth
|
||||||
expense = &entity.Expense{
|
expense = &entity.Expense{
|
||||||
ReferenceNumber: referenceNumber,
|
ReferenceNumber: referenceNumber,
|
||||||
PoNumber: req.PoNumber,
|
PoNumber: req.PoNumber,
|
||||||
Category: req.Category,
|
Category: req.Category,
|
||||||
SupplierId: req.SupplierID,
|
SupplierId: req.SupplierID,
|
||||||
ExpenseDate: expenseDate,
|
TransactionDate: expenseDate,
|
||||||
GrandTotal: grandTotal,
|
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,15 +202,15 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.CostPerKandangs) > 0 {
|
if len(req.ExpenseNonstocks) > 0 {
|
||||||
|
|
||||||
for _, costPerKandang := range req.CostPerKandangs {
|
for _, expenseNonstock := range req.ExpenseNonstocks {
|
||||||
|
|
||||||
var projectFlockKandangId *uint64
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
if req.Category == "BOP" {
|
if req.Category == "BOP" {
|
||||||
|
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
@@ -230,16 +221,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
projectFlockKandangId = &id
|
projectFlockKandangId = &id
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costItem := range costPerKandang.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
|
|
||||||
nonstockId := costItem.NonstockID
|
nonstockId := costItem.NonstockID
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if req.Category == "NON-BOP" {
|
if req.Category == "NON-BOP" {
|
||||||
id := uint64(costPerKandang.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if req.Category == "BOP" {
|
} else if req.Category == "BOP" {
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &costPerKandang.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,8 +240,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
KandangId: kandangId,
|
KandangId: kandangId,
|
||||||
NonstockId: &nonstockId,
|
NonstockId: &nonstockId,
|
||||||
Qty: costItem.Quantity,
|
Qty: costItem.Quantity,
|
||||||
TotalPrice: costItem.TotalCost,
|
Price: costItem.Price,
|
||||||
Note: costItem.Notes,
|
Notes: costItem.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
||||||
@@ -302,9 +293,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -328,10 +317,27 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
|
||||||
}
|
}
|
||||||
updateBody["expense_date"] = expenseDate
|
updateBody["transaction_date"] = expenseDate
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 {
|
if req.Category != nil {
|
||||||
|
updateBody["category"] = *req.Category
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SupplierID != nil {
|
||||||
|
supplierID := uint(*req.SupplierID)
|
||||||
|
supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id)
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 && req.ExpenseNonstocks == nil && len(req.Documents) == 0 {
|
||||||
|
|
||||||
responseDTO, err := s.GetOne(c, id)
|
responseDTO, err := s.GetOne(c, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -346,6 +352,21 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
|
|
||||||
|
currentExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryChanged := false
|
||||||
|
var newCategory string
|
||||||
|
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||||
|
categoryChanged = true
|
||||||
|
newCategory = *req.Category
|
||||||
|
}
|
||||||
|
|
||||||
if len(updateBody) > 0 {
|
if len(updateBody) > 0 {
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -355,41 +376,79 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.CostPerKandang != nil {
|
if categoryChanged {
|
||||||
|
if currentExpense.Category == "BOP" && newCategory == "NON-BOP" {
|
||||||
|
|
||||||
if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items")
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ens := range existingExpenseNonstocks {
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"project_flock_kandang_id": nil,
|
||||||
|
}
|
||||||
|
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" {
|
||||||
|
|
||||||
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
||||||
|
for _, ens := range existingExpenseNonstocks {
|
||||||
|
if ens.KandangId != nil {
|
||||||
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang")
|
||||||
|
}
|
||||||
|
projectFlockKandangId := uint64(projectFlockKandang.Id)
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"project_flock_kandang_id": projectFlockKandangId,
|
||||||
|
}
|
||||||
|
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ExpenseNonstocks != nil {
|
||||||
|
|
||||||
|
var existingExpenseNonstocks []entity.ExpenseNonstock
|
||||||
|
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks for deletion")
|
||||||
}
|
}
|
||||||
|
|
||||||
var grandTotal float64
|
for _, ens := range existingExpenseNonstocks {
|
||||||
for _, cpk := range *req.CostPerKandang {
|
if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil {
|
||||||
for _, costItem := range cpk.CostItems {
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock")
|
||||||
grandTotal += costItem.TotalCost
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{
|
updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
||||||
"grand_total": grandTotal,
|
if err != nil {
|
||||||
}, nil); err != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total")
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cpk := range *req.CostPerKandang {
|
for _, expenseNonstock := range *req.ExpenseNonstocks {
|
||||||
var projectFlockKandangId *uint64
|
var projectFlockKandangId *uint64
|
||||||
|
|
||||||
expense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
|
if updatedExpense.Category == "BOP" {
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
|
||||||
}
|
|
||||||
|
|
||||||
if expense.Category == "BOP" {
|
|
||||||
|
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
|
||||||
@@ -400,7 +459,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
projectFlockKandangId = &id
|
projectFlockKandangId = &id
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, costItem := range cpk.CostItems {
|
for _, costItem := range expenseNonstock.CostItems {
|
||||||
|
|
||||||
nonstockId := uint(costItem.NonstockID)
|
nonstockId := uint(costItem.NonstockID)
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
@@ -410,13 +469,12 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kandangId *uint64
|
var kandangId *uint64
|
||||||
if expense.Category == "NON-BOP" {
|
if updatedExpense.Category == "NON-BOP" {
|
||||||
id := uint64(cpk.KandangID)
|
id := uint64(expenseNonstock.KandangID)
|
||||||
kandangId = &id
|
kandangId = &id
|
||||||
} else if expense.Category == "BOP" {
|
} else if updatedExpense.Category == "BOP" {
|
||||||
|
|
||||||
if projectFlockKandangId != nil {
|
if projectFlockKandangId != nil {
|
||||||
kandangId = &cpk.KandangID
|
kandangId = &expenseNonstock.KandangID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,8 +485,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
KandangId: kandangId,
|
KandangId: kandangId,
|
||||||
NonstockId: &costItem.NonstockID,
|
NonstockId: &costItem.NonstockID,
|
||||||
Qty: costItem.Quantity,
|
Qty: costItem.Quantity,
|
||||||
TotalPrice: costItem.TotalCost,
|
Price: costItem.Price,
|
||||||
Note: costItem.Notes,
|
Notes: costItem.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
||||||
@@ -481,9 +539,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -506,9 +562,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -518,8 +572,6 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy := uint64(1) // TODO: replace with authenticated user id
|
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
@@ -543,13 +595,14 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
|
|
||||||
realization := &entity.ExpenseRealization{
|
realization := &entity.ExpenseRealization{
|
||||||
ExpenseNonstockId: &expenseNonstockID,
|
ExpenseNonstockId: &expenseNonstockID,
|
||||||
RealizationQty: realizationItem.Qty,
|
Qty: realizationItem.Qty,
|
||||||
RealizationUnitPrice: realizationItem.UnitPrice,
|
Price: realizationItem.Price,
|
||||||
RealizationTotalPrice: realizationItem.TotalPrice,
|
Notes: "",
|
||||||
RealizationDate: realizationDate,
|
}
|
||||||
Note: realizationItem.Notes,
|
|
||||||
CreatedBy: &createdBy,
|
if realizationItem.Notes != nil {
|
||||||
|
realization.Notes = *realizationItem.Notes
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
|
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
|
||||||
@@ -576,7 +629,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
expenseID,
|
expenseID,
|
||||||
utils.ExpenseStepRealisasi,
|
utils.ExpenseStepRealisasi,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
uint(createdBy),
|
uint(1), // TODO: replace with authenticated user id
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
@@ -597,9 +650,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -652,14 +703,12 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
|||||||
|
|
||||||
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
s.Log.Errorf("Validation failed for UpdateRealization: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -669,66 +718,56 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
}
|
}
|
||||||
|
|
||||||
if latestApproval != nil && latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) {
|
if latestApproval != nil && (latestApproval.StepNumber < uint16(utils.ExpenseStepRealisasi)) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest,
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Cannot update realization at %s step. Must be at Realisasi step", currentStepName))
|
fmt.Sprintf("tidak bisa update realisasi pada step %s. Harus pada step Realisasi atau selesai", currentStepName))
|
||||||
}
|
}
|
||||||
|
|
||||||
var realizationDate *time.Time
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
if req.RealizationDate != "" {
|
|
||||||
parsedDate, err := utils.ParseDateString(req.RealizationDate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
|
|
||||||
}
|
|
||||||
realizationDate = &parsedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
|
||||||
|
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
for _, realizationItem := range req.Realizations {
|
// Check if only updating documents
|
||||||
|
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
|
||||||
|
|
||||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
if len(req.Realizations) > 0 {
|
||||||
|
for _, realizationItem := range req.Realizations {
|
||||||
|
|
||||||
if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil {
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
|
if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization")
|
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"qty": realizationItem.Qty,
|
||||||
|
"price": realizationItem.Price,
|
||||||
|
}
|
||||||
|
|
||||||
|
if realizationItem.Notes != nil {
|
||||||
|
updateData["notes"] = *realizationItem.Notes
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to update realization: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateData := map[string]interface{}{
|
|
||||||
"realization_qty": realizationItem.Qty,
|
|
||||||
"realization_unit_price": realizationItem.UnitPrice,
|
|
||||||
"realization_total_price": realizationItem.TotalPrice,
|
|
||||||
"realization_date": *realizationDate,
|
|
||||||
}
|
|
||||||
|
|
||||||
if realizationItem.Notes != nil {
|
|
||||||
updateData["note"] = *realizationItem.Notes
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to update realization: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{
|
|
||||||
"realization_date": *realizationDate,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
@@ -737,9 +776,28 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !updateDataOnly && *latestApproval.Action == entity.ApprovalActionUpdated {
|
||||||
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
expenseID,
|
||||||
|
utils.ExpenseStepRealisasi,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "gagal update realisasi expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
responseDTO, err := s.GetOne(c, expenseID)
|
responseDTO, err := s.GetOne(c, expenseID)
|
||||||
@@ -825,9 +883,7 @@ func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx reposito
|
|||||||
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(ctx.Context(),
|
if err := commonSvc.EnsureRelations(ctx.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -909,9 +965,7 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
|
|
||||||
for _, id := range req.ApprovableIds {
|
for _, id := range req.ApprovableIds {
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists},
|
||||||
return s.Repository.IdExists(ctx, uint64(id))
|
|
||||||
}},
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
||||||
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
||||||
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
||||||
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"`
|
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CostPerKandang struct {
|
type ExpenseNonstock struct {
|
||||||
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
|
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
|
||||||
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
|
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,16 @@ type CostPerKandang struct {
|
|||||||
type CostItem struct {
|
type CostItem struct {
|
||||||
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
|
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
|
||||||
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
|
||||||
TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"`
|
Price float64 `form:"price" json:"price" validate:"required,gt=0"`
|
||||||
Notes string `form:"notes" json:"notes" validate:"required,max=500"`
|
Notes string `form:"notes" json:"notes" validate:"required,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -52,8 +54,7 @@ type UpdateRealization struct {
|
|||||||
type RealizationItem struct {
|
type RealizationItem struct {
|
||||||
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
|
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
|
||||||
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
|
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
|
||||||
UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"`
|
Price float64 `form:"price" json:"price" validate:"required,gt=0"`
|
||||||
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
|
|
||||||
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,9 +190,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
|
if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
|
||||||
}
|
}
|
||||||
if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarketingRepository interface {
|
type MarketingRepository interface {
|
||||||
repository.BaseRepository[entity.Marketing]
|
repository.BaseRepository[entity.Marketing]
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetNextSequence(ctx context.Context) (uint, error)
|
GetNextSequence(ctx context.Context) (uint, error)
|
||||||
|
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingRepositoryImpl struct {
|
type MarketingRepositoryImpl struct {
|
||||||
@@ -35,3 +41,82 @@ func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, er
|
|||||||
}
|
}
|
||||||
return maxID + 1, nil
|
return maxID + 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||||
|
return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entity.Marketing{}).
|
||||||
|
Where(fmt.Sprintf("%s = ?", column), value).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) 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.Marketing{}).
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,11 +109,10 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context())
|
soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number")
|
||||||
}
|
}
|
||||||
soNumber := fmt.Sprintf("SO-%05d", nextSeq)
|
|
||||||
|
|
||||||
var marketing *entity.Marketing
|
var marketing *entity.Marketing
|
||||||
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
@@ -321,21 +320,24 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if latestApproval != nil {
|
if latestApproval != nil {
|
||||||
actorID := uint(1) // todo: ambil dari auth context
|
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||||
action := entity.ApprovalActionUpdated
|
actorID := uint(1) // todo: ambil dari auth context
|
||||||
_, err := approvalSvcTx.CreateApproval(
|
action := entity.ApprovalActionUpdated
|
||||||
c.Context(),
|
_, err := approvalSvcTx.CreateApproval(
|
||||||
utils.ApprovalWorkflowMarketing,
|
c.Context(),
|
||||||
id,
|
utils.ApprovalWorkflowMarketing,
|
||||||
approvalutils.ApprovalStep(latestApproval.StepNumber),
|
id,
|
||||||
&action,
|
utils.MarketingStepPengajuan,
|
||||||
actorID,
|
&action,
|
||||||
nil)
|
actorID,
|
||||||
if err != nil {
|
nil)
|
||||||
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval")
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectBudgetRepository interface {
|
||||||
|
repository.BaseRepository[entity.ProjectBudget]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectBudgetRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.ProjectBudget]
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
|
||||||
|
return &ProjectBudgetRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectBudget](db),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,9 @@ const (
|
|||||||
MarketingStepPengajuan approvalutils.ApprovalStep = 1
|
MarketingStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
MarketingStepSalesOrder approvalutils.ApprovalStep = 2
|
MarketingStepSalesOrder approvalutils.ApprovalStep = 2
|
||||||
MarketingDeliveryOrder approvalutils.ApprovalStep = 3
|
MarketingDeliveryOrder approvalutils.ApprovalStep = 3
|
||||||
|
|
||||||
|
MarketingSoNumberPrefix = "SO-"
|
||||||
|
MarketingNumberPadding = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
|||||||
Reference in New Issue
Block a user