mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
Feat[BE-261,265]: createing BOP and BOP realization(Unfinished)
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
CREATE TABLE expenses (
|
CREATE TABLE expenses (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
reference_number VARCHAR, -- format => BOP-LTI-0001 = 0001 is increment
|
reference_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
supplier_id BIGINT NULL,
|
supplier_id BIGINT NULL,
|
||||||
category VARCHAR(50) NOT NULL CHECK (
|
category VARCHAR(50) NOT NULL CHECK (
|
||||||
category IN ('BOP', 'NON-BOP')
|
category IN ('BOP', 'NON-BOP')
|
||||||
),
|
),
|
||||||
po_number VARCHAR(50) UNIQUE NOT NULL,
|
po_number VARCHAR(50) NULL,
|
||||||
document_path JSON,
|
document_path JSON,
|
||||||
expense_date DATE NOT NULL,
|
expense_date DATE NOT NULL,
|
||||||
grand_total NUMERIC(15, 3) DEFAULT 0,
|
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||||
@@ -16,6 +16,8 @@ CREATE TABLE expenses (
|
|||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE SEQUENCE expenses_ref_seq INCREMENT BY 1 START WITH 1;
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke suppliers
|
-- Tambahkan Foreign Key ke suppliers
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -23,7 +25,9 @@ BEGIN
|
|||||||
ALTER TABLE expenses
|
ALTER TABLE expenses
|
||||||
ADD CONSTRAINT fk_expenses_supplier_id
|
ADD CONSTRAINT fk_expenses_supplier_id
|
||||||
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
|
||||||
END IF;
|
|
||||||
|
END IF;
|
||||||
|
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke users (created_by)
|
-- Tambahkan Foreign Key ke users (created_by)
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
DROP TABLE IF EXISTS expense_nonstocks;
|
DROP TABLE IF EXISTS expense_nonstocks;
|
||||||
|
|
||||||
|
DROP SEQUENCE expenses_ref_seq;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
CREATE TABLE expense_nonstocks (
|
CREATE TABLE expense_nonstocks (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
expense_id BIGINT,
|
expense_id BIGINT NOT NULL,
|
||||||
project_flock_kandang_id BIGINT,
|
project_flock_kandang_id BIGINT NULL,
|
||||||
|
kandang_id BIGINT NULL,
|
||||||
nonstock_id BIGINT,
|
nonstock_id BIGINT,
|
||||||
qty NUMERIC(15, 3) NOT NULL,
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
unit_price NUMERIC(15, 3) NOT NULL,
|
unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
@@ -32,6 +33,16 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- Tambahkan Foreign key ke kandang_id
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN
|
||||||
|
ALTER TABLE expense_nonstocks
|
||||||
|
ADD CONSTRAINT fk_expense_nonstocks_kandang_id_2
|
||||||
|
FOREIGN KEY (kandang_id) REFERENCES kandangs(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- Tambahkan Foreign Key ke nonstocks
|
-- Tambahkan Foreign Key ke nonstocks
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE expense_realizations (
|
CREATE TABLE expense_realizations (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
expense_nonstock_id BIGINT,
|
expense_nonstock_id BIGINT UNIQUE,
|
||||||
realization_qty NUMERIC(15, 3) NOT NULL,
|
realization_qty NUMERIC(15, 3) NOT NULL,
|
||||||
realization_unit_price NUMERIC(15, 3) NOT NULL,
|
realization_unit_price NUMERIC(15, 3) NOT NULL,
|
||||||
realization_total_price NUMERIC(15, 3) NOT NULL,
|
realization_total_price NUMERIC(15, 3) NOT NULL,
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
|
|
||||||
type Expense struct {
|
type Expense struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ReferenceNumber *string `gorm:"type:varchar(50)"`
|
ReferenceNumber *string `gorm:"type:varchar(50);uniqueIndex"`
|
||||||
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:"uniqueIndex;not null;type:varchar(50)"`
|
PoNumber *string `gorm:"type:varchar(50)"`
|
||||||
DocumentPath sql.NullString `gorm:"type:json"`
|
DocumentPath sql.NullString `gorm:"type:json"`
|
||||||
ExpenseDate time.Time `gorm:"type:date;not null"`
|
ExpenseDate time.Time `gorm:"type:date;not null"`
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ type ExpenseNonstock struct {
|
|||||||
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"`
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
Realizations []ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
@@ -29,7 +32,7 @@ func (u *ExpenseController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ func (u *ExpenseController) GetAll(c *fiber.Ctx) error {
|
|||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToExpenseListDTOs(result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +74,39 @@ func (u *ExpenseController) GetOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get expense successfully",
|
Message: "Get expense successfully",
|
||||||
Data: dto.ToExpenseListDTO(*result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
req := new(validation.Create)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
req.TransactionDate = c.FormValue("transaction_date")
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
|
supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
|
||||||
|
}
|
||||||
|
req.SupplierID = supplierID
|
||||||
|
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||||
|
}
|
||||||
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
|
costPerKandangJSON := c.FormValue("cost_per_kandangs")
|
||||||
|
if costPerKandangJSON != "" {
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil {
|
||||||
|
|
||||||
|
var singleCostPerKandang validation.CostPerKandang
|
||||||
|
if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ExpenseService.CreateOne(c, req)
|
result, err := u.ExpenseService.CreateOne(c, req)
|
||||||
@@ -92,7 +119,7 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusCreated,
|
Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Create expense successfully",
|
Message: "Create expense successfully",
|
||||||
Data: dto.ToExpenseListDTO(*result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +146,7 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Update expense successfully",
|
Message: "Update expense successfully",
|
||||||
Data: dto.ToExpenseListDTO(*result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,3 +169,126 @@ func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
|
|||||||
Message: "Delete expense successfully",
|
Message: "Delete expense successfully",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) ApproveExpense(c *fiber.Ctx) error {
|
||||||
|
expenseID := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(expenseID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract step from URL path (manager or finance)
|
||||||
|
path := c.Path()
|
||||||
|
var step string
|
||||||
|
if strings.Contains(path, "/approvals/manager") {
|
||||||
|
step = "Manager"
|
||||||
|
} else if strings.Contains(path, "/approvals/finance") {
|
||||||
|
step = "Finance"
|
||||||
|
} else {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval step")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse approval request
|
||||||
|
var req validation.ApprovalRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve expense
|
||||||
|
expense, err := u.ExpenseService.ApproveExpense(c, uint(id), step, req.Action, req.Notes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Approve expense successfully",
|
||||||
|
Data: expense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
|
||||||
|
expenseID := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(expenseID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.CreateRealization)
|
||||||
|
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||||
|
}
|
||||||
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
|
// Parse realizations JSON
|
||||||
|
realizationsJSON := c.FormValue("realizations")
|
||||||
|
if realizationsJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expense, err := u.ExpenseService.CreateRealization(c, uint(id), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create realization successfully",
|
||||||
|
Data: expense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
||||||
|
expenseID := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(expenseID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req validation.UpdateRealization
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update realization successfully",
|
||||||
|
Data: expense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error {
|
||||||
|
expenseID := c.Params("id")
|
||||||
|
id, err := strconv.Atoi(expenseID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
expense, err := u.ExpenseService.CompleteExpense(c, uint(id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Complete expense successfully",
|
||||||
|
Data: expense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,68 +1,322 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"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"
|
||||||
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === Base DTO ===
|
||||||
|
|
||||||
type ExpenseBaseDTO struct {
|
type ExpenseBaseDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
PoNumber string `json:"po_number"`
|
ReferenceNumber string `json:"reference_number"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
PoNumber *string `json:"po_number,omitempty"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
Category string `json:"category"`
|
||||||
|
ExpenseDate time.Time `json:"expense_date"`
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === List DTO (untuk GetAll) ===
|
||||||
|
|
||||||
type ExpenseListDTO struct {
|
type ExpenseListDTO struct {
|
||||||
Id uint64 `json:"id"`
|
ExpenseBaseDTO
|
||||||
ReferenceNumber string `json:"reference_number"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
PoNumber string `json:"po_number"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Category string `json:"category"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ExpenseDate time.Time `json:"expense_date"`
|
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
}
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
// === Detail DTO (untuk GetOne) ===
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
type ExpenseDetailDTO struct {
|
||||||
|
ExpenseBaseDTO
|
||||||
|
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
|
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
||||||
|
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||||
|
TotalRealisasi float64 `json:"total_realisasi"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Nested DTO ===
|
||||||
|
|
||||||
|
type ExpenseNonstockDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"`
|
||||||
|
ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"`
|
||||||
|
Realization *ExpenseRealizationDTO `json:"realization,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockKandangNestedDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
KandangId uint64 `json:"kandang_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseRealizationDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
RealizationQty float64 `json:"realization_qty"`
|
||||||
|
RealizationUnitPrice float64 `json:"realization_unit_price"`
|
||||||
|
RealizationTotalPrice float64 `json:"realization_total_price"`
|
||||||
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RealizationOnlyDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
RealizationQty float64 `json:"realization_qty"`
|
||||||
|
RealizationUnitPrice float64 `json:"realization_unit_price"`
|
||||||
|
RealizationTotalPrice float64 `json:"realization_total_price"`
|
||||||
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
Nonstock *nonstockDTO.NonstockBaseDTO `json:"nonstock,omitempty"`
|
||||||
|
ProjectFlockKandang *ProjectFlockKandangNestedDTO `json:"project_flock_kandang,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KandangDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
KandangId uint64 `json:"kandang_id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KandangGroupDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
KandangId uint64 `json:"kandang_id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"`
|
||||||
|
Realisasi []RealizationOnlyDTO `json:"realisasi,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
func getStringValue(s *string) string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *s
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO {
|
func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
|
||||||
return ExpenseBaseDTO{
|
return ExpenseBaseDTO{
|
||||||
Id: e.Id,
|
|
||||||
PoNumber: e.PoNumber,
|
|
||||||
ExpenseDate: e.ExpenseDate,
|
|
||||||
GrandTotal: e.GrandTotal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
|
||||||
var createdUser *userDTO.UserBaseDTO
|
|
||||||
if e.CreatedUser.Id != 0 {
|
|
||||||
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
|
||||||
createdUser = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExpenseListDTO{
|
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ReferenceNumber: *e.ReferenceNumber,
|
ReferenceNumber: getStringValue(e.ReferenceNumber),
|
||||||
PoNumber: e.PoNumber,
|
PoNumber: e.PoNumber,
|
||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
ExpenseDate: e.ExpenseDate,
|
ExpenseDate: e.ExpenseDate,
|
||||||
GrandTotal: e.GrandTotal,
|
GrandTotal: e.GrandTotal,
|
||||||
CreatedAt: e.CreatedAt,
|
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
CreatedUser: createdUser,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO {
|
func ToExpenseListDTO(e *entity.Expense) ExpenseListDTO {
|
||||||
result := make([]ExpenseListDTO, len(e))
|
var createdUser *userDTO.UserBaseDTO
|
||||||
for i, r := range e {
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
result[i] = ToExpenseListDTO(r)
|
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpenseListDTO{
|
||||||
|
ExpenseBaseDTO: ToExpenseBaseDTO(e),
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO {
|
||||||
|
result := make([]ExpenseListDTO, len(expenses))
|
||||||
|
for i, expense := range expenses {
|
||||||
|
result[i] = ToExpenseListDTO(&expense)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
||||||
|
var createdUser *userDTO.UserBaseDTO
|
||||||
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var supplier *supplierDTO.SupplierBaseDTO
|
||||||
|
if e.Supplier != nil && e.Supplier.Id != 0 {
|
||||||
|
mapped := supplierDTO.ToSupplierBaseDTO(*e.Supplier)
|
||||||
|
supplier = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var pengajuans []ExpenseNonstockDTO
|
||||||
|
var realisasi []RealizationOnlyDTO
|
||||||
|
|
||||||
|
if len(e.Nonstocks) > 0 {
|
||||||
|
pengajuans = make([]ExpenseNonstockDTO, 0)
|
||||||
|
realisasi = make([]RealizationOnlyDTO, 0)
|
||||||
|
|
||||||
|
for _, ns := range e.Nonstocks {
|
||||||
|
// Create DTO without realization for pengajuans
|
||||||
|
pengajuanDTO := ToExpenseNonstockDTO(ns)
|
||||||
|
pengajuanDTO.Realization = nil // Remove realization from pengajuan
|
||||||
|
pengajuans = append(pengajuans, pengajuanDTO)
|
||||||
|
|
||||||
|
// Create separate DTO with realization data if it exists
|
||||||
|
if ns.Realization != nil && ns.Realization.Id != 0 {
|
||||||
|
// Create realization DTO with only realization data
|
||||||
|
var nonstock *nonstockDTO.NonstockBaseDTO
|
||||||
|
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||||
|
mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock)
|
||||||
|
nonstock = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandang *ProjectFlockKandangNestedDTO
|
||||||
|
if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 {
|
||||||
|
projectFlockKandang = &ProjectFlockKandangNestedDTO{
|
||||||
|
Id: uint64(ns.ProjectFlockKandang.Id),
|
||||||
|
KandangId: uint64(ns.ProjectFlockKandang.KandangId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
realisasiDTO := RealizationOnlyDTO{
|
||||||
|
Id: ns.Realization.Id,
|
||||||
|
RealizationQty: ns.Realization.RealizationQty,
|
||||||
|
RealizationUnitPrice: ns.Realization.RealizationUnitPrice,
|
||||||
|
RealizationTotalPrice: ns.Realization.RealizationTotalPrice,
|
||||||
|
RealizationDate: ns.Realization.RealizationDate,
|
||||||
|
Note: ns.Realization.Note,
|
||||||
|
Nonstock: nonstock,
|
||||||
|
ProjectFlockKandang: projectFlockKandang,
|
||||||
|
}
|
||||||
|
realisasi = append(realisasi, realisasiDTO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total pengajuan and realisasi
|
||||||
|
var totalPengajuan float64
|
||||||
|
for _, p := range pengajuans {
|
||||||
|
totalPengajuan += p.TotalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRealisasi float64
|
||||||
|
for _, r := range realisasi {
|
||||||
|
totalRealisasi += r.RealizationTotalPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group pengajuans and realisasi by kandang
|
||||||
|
kandangMap := make(map[uint64]*KandangGroupDTO)
|
||||||
|
|
||||||
|
// Process pengajuans
|
||||||
|
for _, p := range pengajuans {
|
||||||
|
if p.ProjectFlockKandang != nil {
|
||||||
|
kandangId := p.ProjectFlockKandang.KandangId
|
||||||
|
if kandangMap[kandangId] == nil {
|
||||||
|
kandangMap[kandangId] = &KandangGroupDTO{
|
||||||
|
Id: p.ProjectFlockKandang.Id,
|
||||||
|
KandangId: kandangId,
|
||||||
|
Name: fmt.Sprintf("Kandang %d", kandangId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process realisasi
|
||||||
|
for _, r := range realisasi {
|
||||||
|
if r.ProjectFlockKandang != nil {
|
||||||
|
kandangId := r.ProjectFlockKandang.KandangId
|
||||||
|
if kandangMap[kandangId] == nil {
|
||||||
|
kandangMap[kandangId] = &KandangGroupDTO{
|
||||||
|
Id: r.ProjectFlockKandang.Id,
|
||||||
|
KandangId: kandangId,
|
||||||
|
Name: fmt.Sprintf("Kandang %d", kandangId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to slice
|
||||||
|
var kandangs []KandangGroupDTO
|
||||||
|
for _, k := range kandangMap {
|
||||||
|
kandangs = append(kandangs, *k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpenseDetailDTO{
|
||||||
|
ExpenseBaseDTO: ToExpenseBaseDTO(e),
|
||||||
|
Supplier: supplier,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
Kandangs: kandangs,
|
||||||
|
TotalPengajuan: totalPengajuan,
|
||||||
|
TotalRealisasi: totalRealisasi,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
LatestApproval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
|
||||||
|
var nonstock *nonstockDTO.NonstockBaseDTO
|
||||||
|
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
|
||||||
|
mapped := nonstockDTO.ToNonstockBaseDTO(*ns.Nonstock)
|
||||||
|
nonstock = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandang *ProjectFlockKandangNestedDTO
|
||||||
|
if ns.ProjectFlockKandang != nil && ns.ProjectFlockKandang.Id != 0 {
|
||||||
|
projectFlockKandang = &ProjectFlockKandangNestedDTO{
|
||||||
|
Id: uint64(ns.ProjectFlockKandang.Id),
|
||||||
|
KandangId: uint64(ns.ProjectFlockKandang.KandangId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var realization *ExpenseRealizationDTO
|
||||||
|
if ns.Realization != nil && ns.Realization.Id != 0 {
|
||||||
|
mapped := ToExpenseRealizationDTO(*ns.Realization)
|
||||||
|
realization = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpenseNonstockDTO{
|
||||||
|
Id: ns.Id,
|
||||||
|
Qty: ns.Qty,
|
||||||
|
UnitPrice: ns.UnitPrice,
|
||||||
|
TotalPrice: ns.TotalPrice,
|
||||||
|
Note: ns.Note,
|
||||||
|
Nonstock: nonstock,
|
||||||
|
ProjectFlockKandang: projectFlockKandang,
|
||||||
|
Realization: realization,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToExpenseRealizationDTO(r entity.ExpenseRealization) ExpenseRealizationDTO {
|
||||||
|
return ExpenseRealizationDTO{
|
||||||
|
Id: r.Id,
|
||||||
|
RealizationQty: r.RealizationQty,
|
||||||
|
RealizationUnitPrice: r.RealizationUnitPrice,
|
||||||
|
RealizationTotalPrice: r.RealizationTotalPrice,
|
||||||
|
RealizationDate: r.RealizationDate,
|
||||||
|
Note: r.Note,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package expenses
|
package expenses
|
||||||
|
|
||||||
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"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseModule struct{}
|
type ExpenseModule struct{}
|
||||||
@@ -17,10 +27,21 @@ type ExpenseModule struct{}
|
|||||||
func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
expenseRepo := rExpense.NewExpenseRepository(db)
|
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
|
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
expenseService := sExpense.NewExpenseService(expenseRepo, validate)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
|
// Register workflow steps for EXPENSES approval
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ExpenseRoutes(router, userService, expenseService)
|
ExpenseRoutes(router, userService, expenseService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ 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 uint64) (bool, error)
|
||||||
|
GetNextSequence(ctx context.Context) (int, error)
|
||||||
|
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepositoryImpl struct {
|
type ExpenseRepositoryImpl struct {
|
||||||
@@ -26,3 +28,24 @@ 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 uint64) (bool, error) {
|
||||||
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
|
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
|
||||||
|
var sequence int
|
||||||
|
err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return sequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) {
|
||||||
|
var expense entity.Expense
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Preload("Supplier").
|
||||||
|
First(&expense).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &expense, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
type ExpenseNonstockRepository interface {
|
type ExpenseNonstockRepository interface {
|
||||||
repository.BaseRepository[entity.ExpenseNonstock]
|
repository.BaseRepository[entity.ExpenseNonstock]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
|
GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error)
|
||||||
|
GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseNonstockRepositoryImpl struct {
|
type ExpenseNonstockRepositoryImpl struct {
|
||||||
@@ -26,3 +28,28 @@ func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository {
|
|||||||
func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
||||||
return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id))
|
return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseNonstockRepositoryImpl) GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.DB().WithContext(ctx).Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("id = ? AND expense_id = ?", id, expenseID).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseNonstockRepositoryImpl) GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error) {
|
||||||
|
var expenseNonstock entity.ExpenseNonstock
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Preload("Nonstock", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Suppliers")
|
||||||
|
}).
|
||||||
|
First(&expenseNonstock).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &expenseNonstock, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type ExpenseRealizationRepository interface {
|
type ExpenseRealizationRepository interface {
|
||||||
repository.BaseRepository[entity.ExpenseRealization]
|
repository.BaseRepository[entity.ExpenseRealization]
|
||||||
IdExists(ctx context.Context, id uint64) (bool, error)
|
IdExists(ctx context.Context, id uint64) (bool, error)
|
||||||
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -26,3 +27,14 @@ func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository {
|
|||||||
func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
|
||||||
return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id))
|
return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
||||||
|
var realization entity.ExpenseRealization
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("expense_nonstock_id = ?", expenseNonstockID).
|
||||||
|
First(&realization).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &realization, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,4 +25,9 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
route.Post("/:id/approvals/manager", ctrl.ApproveExpense)
|
||||||
|
route.Post("/:id/approvals/finance", ctrl.ApproveExpense)
|
||||||
|
route.Post("/:id/realizations", ctrl.CreateRealization)
|
||||||
|
route.Patch("/:id/realizations", ctrl.UpdateRealization)
|
||||||
|
route.Post("/:id/complete", ctrl.CompleteExpense)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
|
nonstockRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
|
supplierRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -15,33 +25,78 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ExpenseService interface {
|
type ExpenseService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Expense, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Expense, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
ApproveExpense(ctx *fiber.Ctx, id uint, step string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
|
CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
|
CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
|
UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type expenseService struct {
|
type expenseService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ExpenseRepository
|
Repository repository.ExpenseRepository
|
||||||
|
SupplierRepo supplierRepo.SupplierRepository
|
||||||
|
NonstockRepo nonstockRepo.NonstockRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
RealizationRepository repository.ExpenseRealizationRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpenseService(repo repository.ExpenseRepository, validate *validator.Validate) ExpenseService {
|
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService {
|
||||||
return &expenseService{
|
return &expenseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
SupplierRepo: supplierRepo,
|
||||||
|
NonstockRepo: nonstockRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
RealizationRepository: realizationRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("CreatedUser")
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Supplier").
|
||||||
|
Preload("Nonstocks", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstock").Preload("Realization").Preload("ProjectFlockKandang")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) {
|
func (s expenseService) getExpenseWithDetails(c *fiber.Ctx, expenseId uint) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseId, s.withRelations)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Expense not found for ID %d: %+v", expenseId, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get expense for ID %d: %+v", expenseId, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load latest approval with ActionUser
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseId, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// Don't fail if approval loading fails, just log
|
||||||
|
s.Log.Warnf("Failed to load approval for expense %d: %+v", expenseId, err)
|
||||||
|
}
|
||||||
|
expense.LatestApproval = latestApproval
|
||||||
|
|
||||||
|
responseDTO := expenseDto.ToExpenseDetailDTO(expense)
|
||||||
|
return &responseDTO, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
s.Log.Errorf("Validation failed for GetAll: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +105,7 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
return db.Where("category LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -59,57 +114,218 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
s.Log.Errorf("Failed to get expenses: %+v", err)
|
s.Log.Errorf("Failed to get expenses: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return expenses, total, nil
|
|
||||||
|
// Load approvals for each expense
|
||||||
|
for i := range expenses {
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, uint(expenses[i].Id), func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("ActionUser")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Failed to load approval for expense %d: %+v", expenses[i].Id, err)
|
||||||
|
}
|
||||||
|
expenses[i].LatestApproval = latestApproval
|
||||||
|
}
|
||||||
|
|
||||||
|
result := expenseDto.ToExpenseListDTOs(expenses)
|
||||||
|
return result, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*entity.Expense, error) {
|
func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
return s.getExpenseWithDetails(c, id)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
|
||||||
|
func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate supplier exists using common service
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get supplier for category
|
||||||
|
supplierEntity, err := s.SupplierRepo.GetByID(c.Context(), uint(req.SupplierID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed get expense by id: %+v", err)
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
||||||
}
|
}
|
||||||
return expense, nil
|
|
||||||
|
for _, costPerKandang := range req.CostPerKandangs {
|
||||||
|
for _, costItem := range costPerKandang.CostItems {
|
||||||
|
nonstockId := uint(costItem.NonstockID)
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Nonstock", ID: &nonstockId, Exists: s.NonstockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonstockEntity, err := s.NonstockRepo.GetByID(c.Context(), nonstockId, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Suppliers")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Nonstock not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get nonstock")
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierFound := false
|
||||||
|
for _, sn := range nonstockEntity.Suppliers {
|
||||||
|
if uint64(sn.Id) == req.SupplierID {
|
||||||
|
supplierFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !supplierFound {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Nonstock ID %d does not belong to supplier ID %d", nonstockId, req.SupplierID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseDate, err := utils.ParseDateString(req.TransactionDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
|
||||||
|
}
|
||||||
|
|
||||||
|
var expense *entity.Expense
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
expenseRepoTx := repository.NewExpenseRepository(dbTransaction)
|
||||||
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(dbTransaction)
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
|
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
expense = &entity.Expense{
|
||||||
|
ReferenceNumber: &referenceNumber,
|
||||||
|
PoNumber: req.PoNumber,
|
||||||
|
Category: supplierEntity.Category,
|
||||||
|
SupplierId: &req.SupplierID,
|
||||||
|
ExpenseDate: expenseDate,
|
||||||
|
GrandTotal: grandTotal,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := expenseRepoTx.CreateOne(c.Context(), expense, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.CostPerKandangs) > 0 {
|
||||||
|
|
||||||
|
for _, costPerKandang := range req.CostPerKandangs {
|
||||||
|
|
||||||
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, costItem := range costPerKandang.CostItems {
|
||||||
|
|
||||||
|
projectFlockKandangId := uint64(projectFlockKandang.Id)
|
||||||
|
nonstockId := costItem.NonstockID
|
||||||
|
expenseNonstock := &entity.ExpenseNonstock{
|
||||||
|
ExpenseId: &expense.Id,
|
||||||
|
ProjectFlockKandangId: &projectFlockKandangId,
|
||||||
|
NonstockId: &nonstockId,
|
||||||
|
Qty: costItem.Quantity,
|
||||||
|
TotalPrice: costItem.TotalCost,
|
||||||
|
Note: &costItem.Notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense cost item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalAction := entity.ApprovalActionCreated
|
||||||
|
createdByUint := uint(createdBy)
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
uint(expense.Id),
|
||||||
|
utils.ExpenseStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
createdByUint,
|
||||||
|
nil); err != nil {
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
|
||||||
|
} // TODO: Handle documents (save file references)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getExpenseWithDetails(c, uint(expense.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Expense, error) {
|
func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*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 UpdateOne: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy := uint64(1)
|
// Validate expense exists using common service
|
||||||
createBody := &entity.Expense{
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
PoNumber: req.PoNumber,
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
Category: req.Category,
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
CreatedBy: &createdBy,
|
}},
|
||||||
}
|
); err != nil {
|
||||||
|
|
||||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to create expense: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, uint(createBody.Id))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
if req.PoNumber != nil {
|
if req.SupplierID != nil {
|
||||||
updateBody["po_number"] = *req.PoNumber
|
// Validate supplier exists using common service
|
||||||
}
|
supplierID := uint(*req.SupplierID)
|
||||||
if req.Category != nil {
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
updateBody["category"] = *req.Category
|
commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id)
|
||||||
|
}},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody["supplier_id"] = *req.SupplierID
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.getExpenseWithDetails(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
@@ -120,16 +336,368 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
return s.getExpenseWithDetails(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
// Validate expense exists using common service
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
}
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
s.Log.Errorf("Failed to delete expense: %+v", err)
|
}},
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete expense for ID %d: %+v", id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Log.Infof("Successfully deleted expense with ID %d", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) ApproveExpense(c *fiber.Ctx, id uint, stepName string, action string, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
|
}},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
|
|
||||||
|
var stepNumber approvalutils.ApprovalStep
|
||||||
|
switch stepName {
|
||||||
|
case "Manager":
|
||||||
|
stepNumber = utils.ExpenseStepManager
|
||||||
|
case "Finance":
|
||||||
|
stepNumber = utils.ExpenseStepFinance
|
||||||
|
default:
|
||||||
|
s.Log.Errorf("Invalid approval step: %s", stepName)
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval step")
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedPreviousStep uint16
|
||||||
|
switch stepName {
|
||||||
|
case "Manager":
|
||||||
|
expectedPreviousStep = uint16(utils.ExpenseStepPengajuan)
|
||||||
|
case "Finance":
|
||||||
|
expectedPreviousStep = uint16(utils.ExpenseStepManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval.StepNumber != expectedPreviousStep {
|
||||||
|
|
||||||
|
expectedStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(expectedPreviousStep)]
|
||||||
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
|
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Cannot approve at step %s. Latest approval is at %s step. Expected previous step: %s",
|
||||||
|
stepName, currentStepName, expectedStepName))
|
||||||
|
}
|
||||||
|
|
||||||
|
var approvalAction entity.ApprovalAction
|
||||||
|
switch action {
|
||||||
|
case "APPROVED":
|
||||||
|
approvalAction = entity.ApprovalActionApproved
|
||||||
|
case "REJECTED":
|
||||||
|
approvalAction = entity.ApprovalActionRejected
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
id,
|
||||||
|
stepNumber,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
notes); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if stepName == "Finance" && action == "APPROVED" {
|
||||||
|
poNumber, err := s.generatePoNumber(tx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate PO number")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"po_number": poNumber,
|
||||||
|
}
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), id, updateData, nil); err != nil {
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update PO number")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to Approve")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getExpenseWithDetails(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
|
}},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
realizationDate := time.Now()
|
||||||
|
createdBy := uint64(1) // TODO: replace with authenticated user id
|
||||||
|
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
|
|
||||||
|
for _, realizationItem := range req.Realizations {
|
||||||
|
|
||||||
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
|
belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation")
|
||||||
|
}
|
||||||
|
if !belongsToExpense {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
|
||||||
|
if err == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Realization already exists for this expense nonstock")
|
||||||
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing realization")
|
||||||
|
}
|
||||||
|
|
||||||
|
realization := &entity.ExpenseRealization{
|
||||||
|
ExpenseNonstockId: &expenseNonstockID,
|
||||||
|
RealizationQty: realizationItem.Qty,
|
||||||
|
RealizationUnitPrice: realizationItem.UnitPrice,
|
||||||
|
RealizationTotalPrice: realizationItem.TotalPrice,
|
||||||
|
RealizationDate: realizationDate,
|
||||||
|
Note: realizationItem.Notes,
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalAction := entity.ApprovalActionCreated
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
expenseID,
|
||||||
|
utils.ExpenseStepRealisasi,
|
||||||
|
&approvalAction,
|
||||||
|
uint(createdBy),
|
||||||
|
nil); err != nil {
|
||||||
|
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getExpenseWithDetails(c, expenseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
// Validate expense exists using common service
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
|
}},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // TODO: replace with authenticated user id
|
||||||
|
|
||||||
|
// Get latest approval to validate workflow
|
||||||
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Failed to get latest approval for expense %d: %+v", id, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if expense can be completed (must be at Realisasi step)
|
||||||
|
if latestApproval == nil {
|
||||||
|
s.Log.Errorf("No approval found for expense %d", id)
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No approval found. Please create expense first.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) {
|
||||||
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
|
s.Log.Errorf("Cannot complete expense at step %s. Must be at Realisasi step", currentStepName)
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Cannot complete expense at %s step. Must be at Realisasi step", currentStepName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create approval for Selesai step (step 5) using transaction
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
approvalAction := entity.ApprovalActionApproved
|
||||||
|
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowExpense,
|
||||||
|
id,
|
||||||
|
utils.ExpenseStepSelesai,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
notes); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create Selesai approval for expense %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to complete expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getExpenseWithDetails(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
s.Log.Errorf("Validation failed for UpdateRealization: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Expense exists using common service
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return s.Repository.IdExists(ctx, uint64(id))
|
||||||
|
}},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use current date for realization date
|
||||||
|
realizationDate := time.Now()
|
||||||
|
|
||||||
|
// Update realizations using transaction
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
|
||||||
|
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
|
||||||
|
|
||||||
|
// Process each realization item
|
||||||
|
for _, realizationItem := range req.Realizations {
|
||||||
|
// Validate ExpenseNonstock exists and belongs to this expense
|
||||||
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
|
belongsToExpense, err := expenseNonstockRepoTx.GetByExpenseID(c.Context(), uint64(expenseID), uint64(expenseNonstockID))
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to validate ExpenseNonstock relation: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate ExpenseNonstock relation")
|
||||||
|
}
|
||||||
|
if !belongsToExpense {
|
||||||
|
s.Log.Errorf("ExpenseNonstock not found or does not belong to expense %d", expenseID)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "ExpenseNonstock not found or does not belong to this expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing realization
|
||||||
|
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Realization not found for expense nonstock %d", expenseNonstockID)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get existing realization: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated expense
|
||||||
|
return s.getExpenseWithDetails(c, expenseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) generateReferenceNumber(ctx *gorm.DB) (string, error) {
|
||||||
|
|
||||||
|
sequence, err := s.Repository.GetNextSequence(ctx.Statement.Context)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
refNum := fmt.Sprintf("BOP-LTI-%05d", sequence)
|
||||||
|
|
||||||
|
return refNum, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
||||||
|
|
||||||
|
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
||||||
|
expense, err := expenseRepoTx.GetByID(context.Background(), uint(expenseID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if expense.ReferenceNumber == nil {
|
||||||
|
return "", errors.New("reference number is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
poNum := fmt.Sprintf("PO-%s", *expense.ReferenceNumber)
|
||||||
|
|
||||||
|
return poNum, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApprovalRequest is used for expense approval endpoints
|
||||||
|
type ApprovalRequest struct {
|
||||||
|
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
PoNumber string `json:"po_number" validate:"required,max=50"`
|
PoNumber *string `form:"po_number" validate:"omitempty,max=50"`
|
||||||
Category string `json:"category" validate:"required,max=50"`
|
TransactionDate string `form:"transaction_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
SupplierID uint64 `form:"supplier_id" validate:"required,gt=0"`
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"`
|
||||||
|
CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CostPerKandang struct {
|
||||||
|
KandangID uint64 `json:"kandang_id" form:"kandang_id" validate:"required,gt=0"`
|
||||||
|
CostItems []CostItem `json:"cost_items" form:"cost_items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CostItem struct {
|
||||||
|
NonstockID uint64 `json:"nonstock_id" form:"nonstock_id" validate:"required,gt=0"`
|
||||||
|
Quantity float64 `json:"quantity" form:"quantity" validate:"required,gt=0"`
|
||||||
|
TotalCost float64 `json:"total_cost" form:"total_cost" validate:"required,gt=0"`
|
||||||
|
Notes string `json:"notes" form:"notes" validate:"required,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"`
|
PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"`
|
||||||
Category *string `json:"category,omitempty" validate:"omitempty,max=50"`
|
TransactionDate *string `json:"transaction_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
SupplierID *uint64 `json:"supplier_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
|
Documents *[]string `json:"documents,omitempty" validate:"omitempty,dive,max=255"`
|
||||||
|
CostPerKandang *[]CostPerKandang `json:"cost_per_kandang,omitempty" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -15,3 +43,23 @@ type Query struct {
|
|||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateRealization struct {
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" validate:"omitempty,dive"`
|
||||||
|
Realizations []RealizationItem `json:"realizations" form:"realizations" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RealizationItem struct {
|
||||||
|
ExpenseNonstockID uint64 `json:"expense_nonstock_id" form:"expense_nonstock_id" validate:"required,gt=0"`
|
||||||
|
Qty float64 `json:"qty" form:"qty" validate:"required,gt=0"`
|
||||||
|
UnitPrice float64 `json:"unit_price" form:"unit_price" validate:"required,gt=0"`
|
||||||
|
TotalPrice float64 `json:"total_price" form:"total_price" validate:"required,gt=0"`
|
||||||
|
Notes *string `json:"notes" form:"notes" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompleteExpense struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRealization struct {
|
||||||
|
Realizations []RealizationItem `json:"realizations" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type NonstockRepository interface {
|
|||||||
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
|
SyncFlags(ctx context.Context, tx *gorm.DB, nonstockID uint, flags []string) error
|
||||||
DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error
|
DeleteFlags(ctx context.Context, tx *gorm.DB, nonstockID uint) error
|
||||||
GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error)
|
GetFlags(ctx context.Context, nonstockID uint) ([]entity.Flag, error)
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonstockRepositoryImpl struct {
|
type NonstockRepositoryImpl struct {
|
||||||
@@ -34,6 +35,10 @@ func (r *NonstockRepositoryImpl) NameExists(ctx context.Context, name string, ex
|
|||||||
return repository.ExistsByName[entity.Nonstock](ctx, r.DB(), name, excludeID)
|
return repository.ExistsByName[entity.Nonstock](ctx, r.DB(), name, excludeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *NonstockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Nonstock](ctx, r.DB(), id)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
|
func (r *NonstockRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, nonstockID uint, supplierIDs []uint) error {
|
||||||
db := tx
|
db := tx
|
||||||
if db == nil {
|
if db == nil {
|
||||||
|
|||||||
+30
@@ -13,6 +13,7 @@ import (
|
|||||||
type ProjectFlockKandangRepository interface {
|
type ProjectFlockKandangRepository interface {
|
||||||
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
||||||
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||||
|
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||||
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
||||||
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
||||||
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
@@ -220,6 +221,35 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||||
|
record := new(entity.ProjectFlockKandang)
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||||
|
Joins(`
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
||||||
|
FROM approvals
|
||||||
|
WHERE approvable_type = 'PROJECT_FLOCKS'
|
||||||
|
ORDER BY approvable_id, action_at DESC
|
||||||
|
) latest_approval ON latest_approval.approvable_id = project_flocks.id
|
||||||
|
`).
|
||||||
|
Where("project_flock_kandangs.kandang_id = ?", kandangID).
|
||||||
|
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
||||||
|
Order("project_flock_kandangs.id DESC").
|
||||||
|
Preload("ProjectFlock").
|
||||||
|
Preload("ProjectFlock.Fcr").
|
||||||
|
Preload("ProjectFlock.Area").
|
||||||
|
Preload("ProjectFlock.Location").
|
||||||
|
Preload("ProjectFlock.CreatedUser").
|
||||||
|
Preload("ProjectFlock.Kandangs").
|
||||||
|
Preload("ProjectFlock.KandangHistory").
|
||||||
|
Preload("Kandang").
|
||||||
|
First(record).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||||
if len(kandangIDs) == 0 {
|
if len(kandangIDs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -260,12 +260,16 @@ const (
|
|||||||
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
|
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
ExpenseStepManager approvalutils.ApprovalStep = 2
|
ExpenseStepManager approvalutils.ApprovalStep = 2
|
||||||
ExpenseStepFinance approvalutils.ApprovalStep = 3
|
ExpenseStepFinance approvalutils.ApprovalStep = 3
|
||||||
|
ExpenseStepRealisasi approvalutils.ApprovalStep = 4
|
||||||
|
ExpenseStepSelesai approvalutils.ApprovalStep = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
ExpenseStepPengajuan: "Pengajuan",
|
ExpenseStepPengajuan: "Pengajuan",
|
||||||
ExpenseStepManager: "Manager",
|
ExpenseStepManager: "Approval Manager",
|
||||||
ExpenseStepFinance: "Finance",
|
ExpenseStepFinance: "Approval Finance",
|
||||||
|
ExpenseStepRealisasi: "Realisasi",
|
||||||
|
ExpenseStepSelesai: "Selesai",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user