mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 23:35:43 +00:00
Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-284/Report-counting-sapronak
This commit is contained in:
@@ -3,7 +3,7 @@ package capabilities
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
permission "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
@@ -37,8 +37,8 @@ func normalizeAndAllow(perm string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowed = map[string]struct{}{
|
var allowed = map[string]struct{}{
|
||||||
recordings.PermissionRecordingRead: {},
|
permission.PermissionRecordingRead: {},
|
||||||
recordings.PermissionRecordingCreate: {},
|
permission.PermissionRecordingCreate: {},
|
||||||
recordings.PermissionRecordingUpdate: {},
|
permission.PermissionRecordingUpdate: {},
|
||||||
recordings.PermissionRecordingDelete: {},
|
permission.PermissionRecordingDelete: {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dipakai untuk semua module yang butuh cek:
|
||||||
|
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
|
||||||
|
func EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
productWarehouseIDs []uint,
|
||||||
|
) error {
|
||||||
|
if len(productWarehouseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
|
||||||
|
wRepo := warehouseRepo.NewWarehouseRepository(db)
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seenPW := make(map[uint]struct{})
|
||||||
|
seenKandang := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, pwID := range productWarehouseIDs {
|
||||||
|
if pwID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenPW[pwID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenPW[pwID] = struct{}{}
|
||||||
|
|
||||||
|
pw, err := pwRepo.GetByID(ctx, pwID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warehouse tanpa kandang → bukan kandang produksi → skip
|
||||||
|
if wh.KandangId == nil || *wh.KandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(*wh.KandangId)
|
||||||
|
if _, ok := seenKandang[kandangID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenKandang[kandangID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// nggak ada project aktif untuk kandang ini → aman
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
|
||||||
|
if pfk != nil && pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
pfkIDs []uint,
|
||||||
|
) error {
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
for _, id := range pfkIDs {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS closed_at;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
|
||||||
|
ON project_flock_kandangs (closed_at);
|
||||||
+1
-1
@@ -20,7 +20,7 @@ ALTER TABLE product_warehouses
|
|||||||
|
|
||||||
-- Restore audit/soft-delete columns
|
-- Restore audit/soft-delete columns
|
||||||
ALTER TABLE product_warehouses
|
ALTER TABLE product_warehouses
|
||||||
ADD COLUMN IF NOT EXISTS created_by BIGINT NOT NULL REFERENCES users (id),
|
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove grading details from recording_eggs
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP COLUMN IF EXISTS weight;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
|
||||||
|
|
||||||
|
-- Restore grading_eggs table for rollback scenarios
|
||||||
|
CREATE TABLE grading_eggs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_egg_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15,3) NOT NULL,
|
||||||
|
grade VARCHAR,
|
||||||
|
created_by BIGINT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_grading_eggs_recording_egg
|
||||||
|
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_grading_eggs_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_grading_eggs_recording_egg
|
||||||
|
ON grading_eggs (recording_egg_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Remove separate grading table and move grading details into recording_eggs
|
||||||
|
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
|
||||||
|
DROP TABLE IF EXISTS grading_eggs;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
|
||||||
|
|
||||||
|
ALTER TABLE recording_eggs
|
||||||
|
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
|
||||||
|
qty >= 0 AND (weight IS NULL OR weight >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
|
||||||
|
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP COLUMN IF EXISTS expense_nonstock_id,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id,
|
||||||
|
ALTER COLUMN vehicle_number DROP NOT NULL,
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
|
||||||
|
ALTER COLUMN created_at DROP DEFAULT,
|
||||||
|
ALTER COLUMN updated_at DROP DEFAULT;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT,
|
||||||
|
ALTER COLUMN grand_total DROP DEFAULT;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Adjust purchases table to new purchasing schema
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
|
||||||
|
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
|
||||||
|
ALTER COLUMN created_at SET DEFAULT now(),
|
||||||
|
ALTER COLUMN updated_at SET DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term,
|
||||||
|
DROP COLUMN IF EXISTS grand_total;
|
||||||
|
|
||||||
|
-- Bring purchase_items in line with new requirements
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
UPDATE purchase_items
|
||||||
|
SET vehicle_number = ''
|
||||||
|
WHERE vehicle_number IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
|
||||||
|
ALTER COLUMN vehicle_number SET NOT NULL;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_expense_nonstock
|
||||||
|
FOREIGN KEY (expense_nonstock_id)
|
||||||
|
REFERENCES expense_nonstocks(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
|
||||||
|
ON purchase_items (expense_nonstock_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
|
||||||
|
ON purchase_items (project_flock_kandang_id);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Drop function and sequence for sales order numbers
|
||||||
|
DROP FUNCTION IF EXISTS generate_so_number();
|
||||||
|
DROP SEQUENCE IF EXISTS so_number_seq;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Create sequence for sales order numbers
|
||||||
|
CREATE SEQUENCE so_number_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION generate_so_number()
|
||||||
|
RETURNS VARCHAR AS $$
|
||||||
|
DECLARE
|
||||||
|
next_val INTEGER;
|
||||||
|
BEGIN
|
||||||
|
next_val := nextval('so_number_seq');
|
||||||
|
RETURN 'SO-' || LPAD(next_val::TEXT, 5, '0');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT;
|
||||||
@@ -20,5 +20,6 @@ type Kandang struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
|
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
Period int `gorm:"not null"`
|
Period int `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Purchase struct {
|
type Purchase struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PrNumber string `gorm:"not null"`
|
PrNumber string `gorm:"not null"`
|
||||||
PoNumber *string
|
PoNumber *string
|
||||||
PoDate *time.Time
|
PoDate *time.Time
|
||||||
SupplierId uint `gorm:"not null"`
|
SupplierId uint `gorm:"not null"`
|
||||||
CreditTerm *int
|
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
|
||||||
DueDate *time.Time
|
DueDate *time.Time
|
||||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
|
||||||
Notes *string
|
Notes *string
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -5,22 +5,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseItem struct {
|
type PurchaseItem struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
PurchaseId uint `gorm:"not null"`
|
PurchaseId uint `gorm:"not null"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"not null"`
|
||||||
ProductWarehouseId *uint
|
ProductWarehouseId *uint
|
||||||
ReceivedDate *time.Time
|
ProjectFlockKandangId *uint
|
||||||
TravelNumber *string
|
ReceivedDate *time.Time
|
||||||
TravelNumberDocs *string
|
TravelNumber *string
|
||||||
VehicleNumber *string
|
TravelNumberDocs *string
|
||||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
VehicleNumber *string
|
||||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
ExpenseNonstockId *uint64
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -7,24 +7,11 @@ type RecordingEgg struct {
|
|||||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
Qty int `gorm:"column:qty;not null"`
|
Qty int `gorm:"column:qty;not null"`
|
||||||
|
Weight *float64 `gorm:"column:weight"`
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GradingEgg struct {
|
|
||||||
Id uint `gorm:"primaryKey"`
|
|
||||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
|
||||||
Qty float64 `gorm:"column:qty;not null"`
|
|
||||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
|
||||||
CreatedBy uint `gorm:"column:created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
|
|
||||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
|
||||||
}
|
|
||||||
|
|||||||
+127
-60
@@ -3,14 +3,13 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
// "gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
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/modules/sso/session"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -32,66 +31,65 @@ type AuthContext struct {
|
|||||||
// fine-grained authorization using the SSO access token scopes.
|
// fine-grained authorization using the SSO access token scopes.
|
||||||
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
|
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
token := bearerToken(c)
|
// token := bearerToken(c)
|
||||||
if token == "" {
|
// if token == "" {
|
||||||
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
|
// token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
|
||||||
}
|
// }
|
||||||
if token == "" {
|
// if token == "" {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
|
|
||||||
verification, err := sso.VerifyAccessToken(token)
|
// verification, err := sso.VerifyAccessToken(token)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
utils.Log.WithError(err).Warn("auth: token verification failed")
|
// utils.Log.WithError(err).Warn("auth: token verification failed")
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if verification.UserID == 0 {
|
// if verification.UserID == 0 {
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
|
// return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err := ensureNotRevoked(c, token, verification); err != nil {
|
// if err := ensureNotRevoked(c, token, verification); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
user, err := userService.GetBySSOUserID(c, verification.UserID)
|
// user, err := userService.GetBySSOUserID(c, verification.UserID)
|
||||||
if err != nil || user == nil {
|
// if err != nil || user == nil {
|
||||||
utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
|
// utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if len(requiredScopes) > 0 {
|
// if len(requiredScopes) > 0 {
|
||||||
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
|
// if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
var roles []sso.Role
|
// var roles []sso.Role
|
||||||
permissions := make(map[string]struct{})
|
// permissions := make(map[string]struct{})
|
||||||
if verification.UserID != 0 {
|
// if verification.UserID != 0 {
|
||||||
if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
|
// if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
|
||||||
utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
|
// utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
|
||||||
} else if profile != nil {
|
// } else if profile != nil {
|
||||||
roles = profile.Roles
|
// roles = profile.Roles
|
||||||
for _, perm := range profile.PermissionNames() {
|
// for _, perm := range profile.PermissionNames() {
|
||||||
if perm != "" {
|
// if perm != "" {
|
||||||
permissions[perm] = struct{}{}
|
// permissions[perm] = struct{}{}
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
ctx := &AuthContext{
|
// ctx := &AuthContext{
|
||||||
Token: token,
|
// Token: token,
|
||||||
Verification: verification,
|
// Verification: verification,
|
||||||
User: user,
|
// User: user,
|
||||||
Roles: roles,
|
// Roles: roles,
|
||||||
Permissions: permissions,
|
// Permissions: permissions,
|
||||||
}
|
// }
|
||||||
|
|
||||||
c.Locals(authContextLocalsKey, ctx)
|
|
||||||
c.Locals(authUserLocalsKey, user)
|
|
||||||
|
|
||||||
|
// c.Locals(authContextLocalsKey, ctx)
|
||||||
|
// c.Locals(authUserLocalsKey, user)
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,11 +104,12 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
user, ok := AuthenticatedUser(c)
|
// user, ok := AuthenticatedUser(c)
|
||||||
if !ok || user == nil || user.Id == 0 {
|
// if !ok || user == nil || user.Id == 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
// }
|
||||||
return user.Id, nil
|
// return user.Id, nil
|
||||||
|
return 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthDetails returns the full authentication context (token, claims, user).
|
// AuthDetails returns the full authentication context (token, claims, user).
|
||||||
@@ -199,3 +198,71 @@ func hasAllScopes(have, required []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
|
required := canonicalPermissions(perms)
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userPerms := ctx.permissionSet()
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range required {
|
||||||
|
if _, has := userPerms[perm]; !has {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
|
if a == nil || a.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissions(perms []string) []string {
|
||||||
|
out := make([]string, 0, len(perms))
|
||||||
|
seen := make(map[string]struct{}, len(perms))
|
||||||
|
for _, perm := range perms {
|
||||||
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermission(perm string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,14 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
//project-flock
|
||||||
"strings"
|
const (
|
||||||
|
PermissionProjectFlockClosing = "lti:project-flock:closing"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
//recording
|
||||||
func RequirePermissions(perms ...string) fiber.Handler {
|
const (
|
||||||
required := canonicalPermissions(perms)
|
PermissionRecordingRead = "recording.index"
|
||||||
return func(c *fiber.Ctx) error {
|
PermissionRecordingCreate = "recording.create"
|
||||||
if len(required) == 0 {
|
PermissionRecordingUpdate = "recording.update"
|
||||||
return c.Next()
|
PermissionRecordingDelete = "recording.delete"
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
userPerms := ctx.permissionSet()
|
|
||||||
if len(userPerms) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, perm := range required {
|
|
||||||
if _, has := userPerms[perm]; !has {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission reports whether the current request context includes the given permission.
|
|
||||||
func HasPermission(c *fiber.Ctx, perm string) bool {
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm = canonicalPermission(perm)
|
|
||||||
if perm == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, has := ctx.permissionSet()[perm]
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthContext) permissionSet() map[string]struct{} {
|
|
||||||
if a == nil || a.Permissions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermissions(perms []string) []string {
|
|
||||||
out := make([]string, 0, len(perms))
|
|
||||||
seen := make(map[string]struct{}, len(perms))
|
|
||||||
for _, perm := range perms {
|
|
||||||
if canonical := canonicalPermission(perm); canonical != "" {
|
|
||||||
if _, ok := seen[canonical]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[canonical] = struct{}{}
|
|
||||||
out = append(out, canonical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermission(perm string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(perm))
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
@@ -43,17 +44,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
|
JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get all closings successfully",
|
Message: "Retrieved closing projects list successfully",
|
||||||
Meta: response.Meta{
|
Meta: response.Meta{
|
||||||
Page: query.Page,
|
Page: query.Page,
|
||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToClosingListDTOs(result),
|
Data: result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +129,70 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get overhead successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &validation.ClosingSapronakQuery{
|
||||||
|
Type: strings.ToLower(c.Query("type")),
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved closing report (sapronak) successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_id")
|
param := c.Params("project_flock_id")
|
||||||
flag := c.Query("flag", "")
|
flag := c.Query("flag", "")
|
||||||
|
|||||||
@@ -27,20 +27,35 @@ type ClosingDetailDTO struct {
|
|||||||
ClosingListDTO
|
ClosingListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingListItemDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
LocationID uint `json:"location_id"`
|
||||||
|
LocationName string `json:"location_name"`
|
||||||
|
ProjectCategory string `json:"project_category"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
ClosingDate string `json:"closing_date"`
|
||||||
|
ShedLabel string `json:"shed_label"`
|
||||||
|
ShedCount int `json:"shed_count"`
|
||||||
|
SalesPaidAmount int64 `json:"sales_paid_amount"`
|
||||||
|
SalesRemainingAmount int64 `json:"sales_remaining_amount"`
|
||||||
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
|
ProjectStatus string `json:"project_status"`
|
||||||
|
}
|
||||||
|
|
||||||
type ClosingSummaryDTO struct {
|
type ClosingSummaryDTO struct {
|
||||||
LocationID uint `json:"location_id"`
|
FlockID uint `json:"flock_id"`
|
||||||
Periode int `json:"periode"`
|
Period int `json:"period"`
|
||||||
JenisProduk string `json:"jenis_produk"`
|
// JenisProduk string `json:"jenis_produk"`
|
||||||
LabelPopulasi string `json:"label_populasi"`
|
// LabelPopulasi string `json:"label_populasi"`
|
||||||
JumlahPopulasi int `json:"jumlah_populasi"`
|
Population int `json:"population"`
|
||||||
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
|
PopulationFormatted string `json:"population_formatted"`
|
||||||
JenisProject string `json:"jenis_project"`
|
ProjectType string `json:"project_type"`
|
||||||
KandangAktif int `json:"kandang_aktif"`
|
ActiveHouseCount int `json:"active_house_count"`
|
||||||
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
|
ActiveHouseLabel string `json:"active_house_label"`
|
||||||
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
|
SalesPaymentStatus string `json:"sales_payment_status"`
|
||||||
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||||
StatusProject string `json:"status_project"`
|
StatusProject string `json:"project_status"`
|
||||||
StatusClosing string `json:"status_closing"`
|
StatusClosing string `json:"closing_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||||
@@ -52,19 +67,38 @@ func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosi
|
|||||||
populationInt := int(population)
|
populationInt := int(population)
|
||||||
|
|
||||||
return ClosingSummaryDTO{
|
return ClosingSummaryDTO{
|
||||||
LocationID: project.LocationId,
|
FlockID: project.Id,
|
||||||
Periode: period,
|
Period: period,
|
||||||
JenisProduk: project.Category,
|
// JenisProduk: project.Category,
|
||||||
LabelPopulasi: "",
|
// LabelPopulasi: "",
|
||||||
JumlahPopulasi: populationInt,
|
Population: populationInt,
|
||||||
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||||
JenisProject: "",
|
ProjectType: project.Category,
|
||||||
KandangAktif: kandangCount,
|
ActiveHouseCount: kandangCount,
|
||||||
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
|
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
|
||||||
StatusPembayaranPenjualan: "Tempo",
|
SalesPaymentStatus: "Tempo",
|
||||||
StatusPembayaranMitra: "",
|
// StatusPembayaranMitra: "",
|
||||||
StatusProject: statusProject,
|
StatusProject: statusProject,
|
||||||
StatusClosing: statusClosing,
|
StatusClosing: statusClosing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
|
||||||
|
shedCount := len(project.KandangHistory)
|
||||||
|
|
||||||
|
return ClosingListItemDTO{
|
||||||
|
Id: project.Id,
|
||||||
|
LocationID: project.LocationId,
|
||||||
|
LocationName: project.Location.Name,
|
||||||
|
ProjectCategory: project.Category,
|
||||||
|
Period: maxPeriod(project.KandangHistory),
|
||||||
|
ClosingDate: "17-Nov-2025",
|
||||||
|
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
|
||||||
|
ShedCount: shedCount,
|
||||||
|
SalesPaidAmount: 21993726,
|
||||||
|
SalesRemainingAmount: 11075919,
|
||||||
|
SalesPaymentStatus: "Lunas",
|
||||||
|
ProjectStatus: projectStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === Response DTO ===
|
// === Response DTO ===
|
||||||
|
|
||||||
type SalesDTO struct {
|
type SalesDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
RealizationDate time.Time `json:"realization_date"`
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type OverheadDTO struct {
|
||||||
|
ItemName string `json:"item_name"`
|
||||||
|
UOMName string `json:"uom_name"`
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetUnitPrice float64 `json:"budget_unit_price"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualDate string `json:"actual_date"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualUnitPrice float64 `json:"actual_unit_price"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TotalDTO struct {
|
||||||
|
BudgetQuantity float64 `json:"budget_quantity"`
|
||||||
|
BudgetTotalAmount float64 `json:"budget_total_amount"`
|
||||||
|
ActualQuantity float64 `json:"actual_quantity"`
|
||||||
|
ActualTotalAmount float64 `json:"actual_total_amount"`
|
||||||
|
CostPerBird float64 `json:"cost_per_bird"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverheadListDTO struct {
|
||||||
|
Total TotalDTO `json:"total"`
|
||||||
|
Overheads []OverheadDTO `json:"overheads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
|
||||||
|
if budget == nil && realization == nil {
|
||||||
|
return OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemName, itemUOM string
|
||||||
|
if budget != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(budget.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
|
||||||
|
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := OverheadDTO{
|
||||||
|
ItemName: itemName,
|
||||||
|
UOMName: itemUOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
if budget != nil {
|
||||||
|
dto.BudgetQuantity = budget.Qty
|
||||||
|
dto.BudgetUnitPrice = budget.Price
|
||||||
|
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
if realization != nil {
|
||||||
|
dto.ActualQuantity = realization.Qty
|
||||||
|
dto.ActualUnitPrice = realization.Price
|
||||||
|
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
|
||||||
|
dto.ActualDate = formatRealizationDate(realization)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
|
||||||
|
overheadsByNonstockID := make(map[uint]*OverheadDTO)
|
||||||
|
latestDateByNonstockID := make(map[uint]string)
|
||||||
|
|
||||||
|
for i := range budgets {
|
||||||
|
nonstockID := budgets[i].NonstockId
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
|
||||||
|
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range realizations {
|
||||||
|
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
|
||||||
|
if overheadsByNonstockID[nonstockID] == nil {
|
||||||
|
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
|
||||||
|
}
|
||||||
|
|
||||||
|
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
|
||||||
|
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
|
||||||
|
|
||||||
|
if overheadsByNonstockID[nonstockID].ItemName == "" {
|
||||||
|
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
|
||||||
|
overheadsByNonstockID[nonstockID].ItemName = itemName
|
||||||
|
overheadsByNonstockID[nonstockID].UOMName = itemUOM
|
||||||
|
}
|
||||||
|
|
||||||
|
realizationDateStr := formatRealizationDate(&realizations[i])
|
||||||
|
if realizationDateStr != "" {
|
||||||
|
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
|
||||||
|
latestDateByNonstockID[nonstockID] = realizationDateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
|
||||||
|
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
|
||||||
|
|
||||||
|
for nonstockID, overhead := range overheadsByNonstockID {
|
||||||
|
overhead.ActualDate = latestDateByNonstockID[nonstockID]
|
||||||
|
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
|
||||||
|
|
||||||
|
if overhead.ActualQuantity > 0 {
|
||||||
|
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBudgetQuantity += overhead.BudgetQuantity
|
||||||
|
totalBudgetAmount += overhead.BudgetTotalAmount
|
||||||
|
totalActualQuantity += overhead.ActualQuantity
|
||||||
|
totalActualAmount += overhead.ActualTotalAmount
|
||||||
|
|
||||||
|
overheadItems = append(overheadItems, *overhead)
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverheadListDTO{
|
||||||
|
Total: TotalDTO{
|
||||||
|
BudgetQuantity: totalBudgetQuantity,
|
||||||
|
BudgetTotalAmount: totalBudgetAmount,
|
||||||
|
ActualQuantity: totalActualQuantity,
|
||||||
|
ActualTotalAmount: totalActualAmount,
|
||||||
|
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
|
||||||
|
},
|
||||||
|
Overheads: overheadItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions ===
|
||||||
|
|
||||||
|
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
|
||||||
|
if nonstock != nil && nonstock.Id != 0 {
|
||||||
|
return nonstock.Name, nonstock.Uom.Name
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotal(qty, price float64) float64 {
|
||||||
|
return qty * price
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
|
||||||
|
if totalChickinQty > 0 {
|
||||||
|
return totalPrice / totalChickinQty
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRealizationDate(realization *entity.ExpenseRealization) string {
|
||||||
|
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
|
||||||
|
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
|
||||||
|
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -86,3 +86,26 @@ type SapronakProjectAggregatedDTO struct {
|
|||||||
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
|
||||||
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
ReferenceNumber string `json:"reference_number"`
|
||||||
|
TransactionType string `json:"transaction_type"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
ProductSubCategory string `json:"product_sub_category"`
|
||||||
|
SourceWarehouse string `json:"source_warehouse"`
|
||||||
|
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
|
||||||
|
// Destination string `json:"destination,omitempty"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Unit string `json:"unit"`
|
||||||
|
FormattedQuantity string `json:"formatted_quantity"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
SortDate time.Time `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClosingSapronakDTO struct {
|
||||||
|
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
|
||||||
|
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
@@ -22,12 +24,15 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||||
marketingRepo := rMarketings.NewMarketingRepository(db)
|
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||||
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||||
|
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
|
||||||
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
|
||||||
sapronakService := sClosing.NewSapronakService(closingRepo, validate)
|
sapronakService := sClosing.NewSapronakService(closingRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
@@ -14,7 +15,8 @@ import (
|
|||||||
|
|
||||||
type ClosingRepository interface {
|
type ClosingRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error)
|
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
|
||||||
|
ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error)
|
||||||
MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error)
|
MapSapronakStartDates(ctx context.Context, pfkIDs []uint) (map[uint]time.Time, error)
|
||||||
FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
|
FetchSapronakIncoming(ctx context.Context, kandangID uint, start, end *time.Time) ([]SapronakIncomingRow, error)
|
||||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
|
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, error)
|
||||||
@@ -34,6 +36,202 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SapronakRow struct {
|
||||||
|
Id uint64 `gorm:"column:id"`
|
||||||
|
SortDate time.Time `gorm:"column:sort_date"`
|
||||||
|
DateText string `gorm:"column:date_text"`
|
||||||
|
ReferenceNumber string `gorm:"column:reference_number"`
|
||||||
|
TransactionType string `gorm:"column:transaction_type"`
|
||||||
|
ProductName string `gorm:"column:product_name"`
|
||||||
|
ProductCategory string `gorm:"column:product_category"`
|
||||||
|
ProductSubCategory string `gorm:"column:product_sub_category"`
|
||||||
|
SourceWarehouse string `gorm:"column:source_warehouse"`
|
||||||
|
DestinationWarehouse string `gorm:"column:destination_warehouse"`
|
||||||
|
Destination string `gorm:"column:destination"`
|
||||||
|
Quantity float64 `gorm:"column:quantity"`
|
||||||
|
Unit string `gorm:"column:unit"`
|
||||||
|
Notes string `gorm:"column:notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SapronakQueryParams struct {
|
||||||
|
Type string
|
||||||
|
WarehouseIDs []uint
|
||||||
|
ProjectFlockKandangIDs []uint
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unionParts []string
|
||||||
|
args []any
|
||||||
|
)
|
||||||
|
|
||||||
|
switch params.Type {
|
||||||
|
case validation.SapronakTypeIncoming:
|
||||||
|
if len(params.WarehouseIDs) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||||
|
case validation.SapronakTypeOutgoing:
|
||||||
|
if len(params.WarehouseIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||||
|
args = append(args, params.WarehouseIDs)
|
||||||
|
}
|
||||||
|
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||||
|
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||||
|
args = append(args, params.ProjectFlockKandangIDs)
|
||||||
|
}
|
||||||
|
if len(unionParts) == 0 {
|
||||||
|
return []SapronakRow{}, 0, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionSQL := strings.Join(unionParts, " UNION ALL ")
|
||||||
|
|
||||||
|
var totalResults int64
|
||||||
|
countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL)
|
||||||
|
if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataArgs := append(append([]any{}, args...), params.Limit, params.Offset)
|
||||||
|
dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL)
|
||||||
|
|
||||||
|
var rows []SapronakRow
|
||||||
|
if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
sapronakIncomingPurchasesSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(pi.id AS BIGINT) AS id,
|
||||||
|
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||||
|
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||||
|
COALESCE(p.po_number, '') AS reference_number,
|
||||||
|
'Purchase' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
'External Supplier' AS source_warehouse,
|
||||||
|
w.name AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
pi.total_qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
COALESCE(p.notes, '') AS notes
|
||||||
|
FROM purchase_items pi
|
||||||
|
JOIN purchases p ON p.id = pi.purchase_id
|
||||||
|
JOIN products prod ON prod.id = pi.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pi.warehouse_id
|
||||||
|
WHERE pi.warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakIncomingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer In' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination_warehouse,
|
||||||
|
'' AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Stock Refill' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.to_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingTransfersSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(st.id AS BIGINT) AS id,
|
||||||
|
st.transfer_date AS sort_date,
|
||||||
|
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
st.movement_number AS reference_number,
|
||||||
|
'Internal Transfer Out' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
COALESCE(fw.name, '') AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
COALESCE(tw.name, '') AS destination,
|
||||||
|
std.quantity AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
'Transfer to other unit' AS notes
|
||||||
|
FROM stock_transfer_details std
|
||||||
|
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||||
|
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||||
|
LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = std.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
WHERE st.from_warehouse_id IN ?
|
||||||
|
`
|
||||||
|
|
||||||
|
sapronakOutgoingMarketingsSQL = `
|
||||||
|
SELECT
|
||||||
|
CAST(mp.id AS BIGINT) AS id,
|
||||||
|
m.so_date AS sort_date,
|
||||||
|
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||||
|
m.so_number AS reference_number,
|
||||||
|
'Trading Sales' AS transaction_type,
|
||||||
|
prod.name AS product_name,
|
||||||
|
pc.name AS product_category,
|
||||||
|
COALESCE((
|
||||||
|
SELECT string_agg(f.name, ' ')
|
||||||
|
FROM flags f
|
||||||
|
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||||
|
), '') AS product_sub_category,
|
||||||
|
w.name AS source_warehouse,
|
||||||
|
'' AS destination_warehouse,
|
||||||
|
'RETAIL CUSTOMER' AS destination,
|
||||||
|
mp.qty AS quantity,
|
||||||
|
u.name AS unit,
|
||||||
|
m.notes AS notes
|
||||||
|
FROM marketing_products mp
|
||||||
|
JOIN marketings m ON m.id = mp.marketing_id
|
||||||
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||||
|
JOIN products prod ON prod.id = pw.product_id
|
||||||
|
JOIN product_categories pc ON pc.id = prod.product_category_id
|
||||||
|
JOIN uoms u ON u.id = prod.uom_id
|
||||||
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||||
|
WHERE pw.project_flock_kandang_id IN ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
type SapronakIncomingRow struct {
|
type SapronakIncomingRow struct {
|
||||||
ProductID uint
|
ProductID uint
|
||||||
ProductName string
|
ProductName string
|
||||||
@@ -62,7 +260,7 @@ type SapronakDetailRow struct {
|
|||||||
Price float64
|
Price float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClosingRepositoryImpl) ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
func (r *ClosingRepositoryImpl) ListProjectFlockKandangsForSapronak(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||||
db := r.DB().
|
db := r.DB().
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Preload("ProjectFlock").
|
Preload("ProjectFlock").
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
formatter := closing.NewSapronakFormatter()
|
formatter := closing.NewSapronakFormatter()
|
||||||
ctrl := controller.NewClosingController(s, sapronakSvc, formatter)
|
ctrl := controller.NewClosingController(s, sapronakSvc, formatter)
|
||||||
|
|
||||||
route := v1.Group("/closing")
|
route := v1.Group("/closings")
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
@@ -23,7 +23,9 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||||
|
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
|
||||||
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang)
|
||||||
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject)
|
||||||
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
|
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectflockRepository "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"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -22,10 +25,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClosingService interface {
|
type ClosingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
|
||||||
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
|
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
|
||||||
|
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type closingService struct {
|
type closingService struct {
|
||||||
@@ -36,9 +41,12 @@ type closingService struct {
|
|||||||
MarketingRepo marketingRepository.MarketingRepository
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
|
||||||
|
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
|
||||||
|
ChickinRepo chickinRepository.ProjectChickinRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
|
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
|
||||||
return &closingService{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -47,6 +55,9 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
|
|||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
ProjectBudgetRepo: projectBudgetRepo,
|
||||||
|
ChickinRepo: chickinRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +67,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
|
|
||||||
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||||
return s.withRelations(db).
|
return s.withRelations(db).
|
||||||
|
Preload("Location").
|
||||||
Preload("KandangHistory").
|
Preload("KandangHistory").
|
||||||
Preload("KandangHistory.Chickins")
|
Preload("KandangHistory.Chickins")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -68,9 +80,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withClosingRelations(db)
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
return db.Where("flock_name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
@@ -79,7 +91,19 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
s.Log.Errorf("Failed to get closings: %+v", err)
|
s.Log.Errorf("Failed to get closings: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return closings, total, nil
|
|
||||||
|
result := make([]dto.ClosingListItemDTO, 0, len(closings))
|
||||||
|
for _, closing := range closings {
|
||||||
|
statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, dto.ToClosingListItemDTO(closing, statusProject))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||||
@@ -144,6 +168,147 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
|
|||||||
return &summary, nil
|
return &summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if params == nil {
|
||||||
|
params = &validation.ClosingSapronakQuery{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Page == 0 {
|
||||||
|
params.Page = 1
|
||||||
|
}
|
||||||
|
if params.Limit == 0 {
|
||||||
|
params.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectFlockKandangIDs []uint
|
||||||
|
if params.Type == validation.SapronakTypeOutgoing {
|
||||||
|
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{
|
||||||
|
Type: params.Type,
|
||||||
|
WarehouseIDs: warehouseIDs,
|
||||||
|
ProjectFlockKandangIDs: projectFlockKandangIDs,
|
||||||
|
Limit: params.Limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err)
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data")
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]dto.ClosingSapronakItemDTO, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
dateStr := row.DateText
|
||||||
|
if dateStr == "" && !row.SortDate.IsZero() {
|
||||||
|
dateStr = row.SortDate.Format("02-Jan-2006")
|
||||||
|
}
|
||||||
|
items = append(items, dto.ClosingSapronakItemDTO{
|
||||||
|
Id: row.Id,
|
||||||
|
Date: dateStr,
|
||||||
|
ReferenceNumber: row.ReferenceNumber,
|
||||||
|
TransactionType: row.TransactionType,
|
||||||
|
ProductName: row.ProductName,
|
||||||
|
ProductCategory: row.ProductCategory,
|
||||||
|
ProductSubCategory: row.ProductSubCategory,
|
||||||
|
SourceWarehouse: row.SourceWarehouse,
|
||||||
|
DestinationWarehouse: row.DestinationWarehouse,
|
||||||
|
// Destination: row.Destination,
|
||||||
|
Quantity: row.Quantity,
|
||||||
|
Unit: row.Unit,
|
||||||
|
FormattedQuantity: formatQuantity(row.Quantity, row.Unit),
|
||||||
|
Notes: row.Notes,
|
||||||
|
SortDate: row.SortDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, totalResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var kandangIDs []uint
|
||||||
|
db := s.Repository.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("kandang_id", &kandangIDs).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kandangIDs) == 0 {
|
||||||
|
return []uint{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var warehouses []entity.Warehouse
|
||||||
|
if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := make(map[uint]struct{})
|
||||||
|
for _, warehouse := range warehouses {
|
||||||
|
unique[warehouse.Id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]uint, 0, len(unique))
|
||||||
|
for id := range unique {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) {
|
||||||
|
var ids []uint
|
||||||
|
err := s.Repository.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Pluck("id", &ids).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatQuantity(qty float64, uom string) string {
|
||||||
|
qtyStr := strconv.FormatFloat(qty, 'f', -1, 64)
|
||||||
|
if uom == "" {
|
||||||
|
return qtyStr
|
||||||
|
}
|
||||||
|
return qtyStr + " " + uom
|
||||||
|
}
|
||||||
|
|
||||||
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||||
if s.ApprovalSvc == nil {
|
if s.ApprovalSvc == nil {
|
||||||
return "", "Belum Selesai", nil
|
return "", "Belum Selesai", nil
|
||||||
@@ -188,3 +353,29 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
|||||||
|
|
||||||
return statusProject, statusClosing, nil
|
return statusProject, statusClosing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
|
||||||
|
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalChickinQty float64
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
totalChickinQty += chickin.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
|
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
type SapronakService interface {
|
type SapronakService interface {
|
||||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||||
GetSapronakReport(ctx *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error)
|
GetSapronakReport(ctx *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sapronakService struct {
|
type sapronakService struct {
|
||||||
@@ -36,7 +36,7 @@ func NewSapronakService(repo repository.ClosingRepository, validate *validator.V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
|
func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint,
|
|||||||
if projectFlockID == 0 {
|
if projectFlockID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||||
}
|
}
|
||||||
reports, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{
|
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
ProjectFlockID: projectFlockID,
|
ProjectFlockID: projectFlockID,
|
||||||
Status: "all",
|
Status: "all",
|
||||||
Flag: flag,
|
Flag: flag,
|
||||||
@@ -68,7 +68,7 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint,
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{
|
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||||
ProjectFlockID: projectFlockID,
|
ProjectFlockID: projectFlockID,
|
||||||
ProjectFlockKandangID: pfkID,
|
ProjectFlockKandangID: pfkID,
|
||||||
Status: "all",
|
Status: "all",
|
||||||
@@ -87,7 +87,7 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint,
|
|||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) {
|
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||||
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
pfks, err := s.loadProjectFlockKandangs(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -158,7 +158,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.SapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||||
pfks, err := s.Repository.ListProjectFlockKandangsForSapronak(ctx, params)
|
pfks, err := s.Repository.ListProjectFlockKandangsForSapronak(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
s.Log.Errorf("Failed to load project flock kandangs for sapronak report: %+v", err)
|
||||||
|
|||||||
@@ -37,22 +37,22 @@ func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO, flag st
|
|||||||
result := dto.SapronakProjectAggregatedDTO{}
|
result := dto.SapronakProjectAggregatedDTO{}
|
||||||
|
|
||||||
if report == nil {
|
if report == nil {
|
||||||
return result
|
report = &dto.SapronakReportDTO{}
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := strings.ToUpper(strings.TrimSpace(flag))
|
filter := strings.ToUpper(strings.TrimSpace(flag))
|
||||||
|
|
||||||
byFlag := map[string]**dto.SapronakCategoryDTO{}
|
byFlag := map[string]**dto.SapronakCategoryDTO{}
|
||||||
if filter == "" || filter == "DOC" {
|
if filter == "" || filter == "DOC" {
|
||||||
result.Doc = &dto.SapronakCategoryDTO{}
|
result.Doc = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0)}
|
||||||
byFlag["DOC"] = &result.Doc
|
byFlag["DOC"] = &result.Doc
|
||||||
}
|
}
|
||||||
if filter == "" || filter == "OVK" {
|
if filter == "" || filter == "OVK" {
|
||||||
result.Ovk = &dto.SapronakCategoryDTO{}
|
result.Ovk = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
|
||||||
byFlag["OVK"] = &result.Ovk
|
byFlag["OVK"] = &result.Ovk
|
||||||
}
|
}
|
||||||
if filter == "" || filter == "PAKAN" {
|
if filter == "" || filter == "PAKAN" {
|
||||||
result.Pakan = &dto.SapronakCategoryDTO{}
|
result.Pakan = &dto.SapronakCategoryDTO{Rows: make([]dto.SapronakCategoryRowDTO, 0),}
|
||||||
byFlag["PAKAN"] = &result.Pakan
|
byFlag["PAKAN"] = &result.Pakan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -13,3 +13,14 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SapronakTypeIncoming = "incoming"
|
||||||
|
SapronakTypeOutgoing = "outgoing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClosingSapronakQuery struct {
|
||||||
|
Type string `query:"type" validate:"required,oneof=incoming outgoing"`
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type SapronakQuery struct {
|
type CountSapronakQuery struct {
|
||||||
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
|
||||||
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"`
|
||||||
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
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"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ type ExpenseRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (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)
|
||||||
|
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
|
||||||
|
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepositoryImpl struct {
|
type ExpenseRepositoryImpl struct {
|
||||||
@@ -49,3 +53,57 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
|
|||||||
}
|
}
|
||||||
return &expense, nil
|
return &expense, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
|
||||||
|
if pfkID > 0 && kandangID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
|
||||||
|
}
|
||||||
|
if pfkID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
|
||||||
|
}
|
||||||
|
return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Table("expenses").
|
||||||
|
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
|
||||||
|
Group("expenses.id").Where("expenses.deleted_at IS NULL").
|
||||||
|
Pluck("expenses.id", &ids).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
for _, id := range ids {
|
||||||
|
var latest entity.Approval
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("approvals").
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
|
||||||
|
Order("action_at DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&latest).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if isFinished != nil {
|
||||||
|
if !isFinished(&latest) {
|
||||||
|
unfinished++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unfinished, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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)
|
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRealizationRepositoryImpl struct {
|
type ExpenseRealizationRepositoryImpl struct {
|
||||||
@@ -30,11 +31,22 @@ func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint
|
|||||||
|
|
||||||
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
|
||||||
var realization entity.ExpenseRealization
|
var realization entity.ExpenseRealization
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
|
||||||
Where("expense_nonstock_id = ?", expenseNonstockID).
|
return &realization, err
|
||||||
First(&realization).Error
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
|
||||||
}
|
var realizations []entity.ExpenseRealization
|
||||||
return &realization, nil
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("ExpenseNonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock").
|
||||||
|
Preload("ExpenseNonstock.Nonstock.Uom").
|
||||||
|
Preload("ExpenseNonstock.Expense").
|
||||||
|
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
|
||||||
|
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Where("expenses.category = ?", "BOP").
|
||||||
|
Find(&realizations).Error
|
||||||
|
return realizations, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
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"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
|
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"
|
||||||
@@ -183,12 +184,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
|||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
|
||||||
referenceNumber, err := s.generateReferenceNumber(dbTransaction)
|
referenceNumber, err := GenerateExpenseReferenceNumber(c.Context(), expenseRepoTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy := uint64(1) //todo get from auth
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
createdBy := uint64(actorID)
|
||||||
expense = &entity.Expense{
|
expense = &entity.Expense{
|
||||||
ReferenceNumber: referenceNumber,
|
ReferenceNumber: referenceNumber,
|
||||||
PoNumber: req.PoNumber,
|
PoNumber: req.PoNumber,
|
||||||
@@ -360,6 +365,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
categoryChanged := false
|
categoryChanged := false
|
||||||
var newCategory string
|
var newCategory string
|
||||||
if req.Category != nil && *req.Category != currentExpense.Category {
|
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||||
@@ -404,6 +412,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if ens.KandangId != nil {
|
if ens.KandangId != nil {
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -496,7 +507,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
if *latestApproval.Action != entity.ApprovalActionUpdated {
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionUpdated
|
approvalAction := entity.ApprovalActionUpdated
|
||||||
@@ -543,7 +557,21 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if 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 get expense for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
@@ -572,6 +600,20 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
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))
|
||||||
@@ -655,7 +697,10 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -712,7 +757,19 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
@@ -960,11 +1017,14 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: replace with authenticated user id
|
actorID, err := middleware.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
|
||||||
|
}
|
||||||
|
|
||||||
var results []expenseDto.ExpenseDetailDTO
|
var results []expenseDto.ExpenseDetailDTO
|
||||||
|
|
||||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
@@ -1010,6 +1070,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||||
}
|
}
|
||||||
|
if approvalAction == entity.ApprovalActionApproved {
|
||||||
|
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -1056,17 +1131,6 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func (s *expenseService) generatePoNumber(ctx *gorm.DB, expenseID uint) (string, error) {
|
||||||
|
|
||||||
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
expenseRepoTx := repository.NewExpenseRepository(ctx)
|
||||||
@@ -1090,13 +1154,45 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
||||||
// user, ok := authmiddleware.AuthenticatedUser(c)
|
ctx context.Context,
|
||||||
// if !ok || user == nil || user.Id == 0 {
|
expense *entity.Expense,
|
||||||
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
) error {
|
||||||
// }
|
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
||||||
// return user.Id, nil
|
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
||||||
// }
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// return user.Id, nil
|
seen := make(map[uint]struct{})
|
||||||
// }
|
|
||||||
|
for _, ens := range expense.Nonstocks {
|
||||||
|
// Field ini pointer, bisa nil
|
||||||
|
if ens.ProjectFlockKandangId == nil || *ens.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID := uint(*ens.ProjectFlockKandangId)
|
||||||
|
if _, ok := seen[pfkID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pfkID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, pfkID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock %d tidak ditemukan", pfkID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate project flock %d for expense %d: %+v", pfkID, expense.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// ❗ RULE: kalau ClosedAt tidak nil → project sudah closing
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateExpenseReferenceNumber builds a new reference number using the expense sequence.
|
||||||
|
func GenerateExpenseReferenceNumber(ctx context.Context, repo expenseRepo.ExpenseRepository) (string, error) {
|
||||||
|
sequence, err := repo.GetNextSequence(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("BOP-LTI-%05d", sequence), nil
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
@@ -21,10 +21,11 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
productRepo := rproduct.NewProductRepository(db)
|
productRepo := rproduct.NewProductRepository(db)
|
||||||
|
|
||||||
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
|
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
AdjustmentRoutes(router, userService, adjustmentService)
|
AdjustmentRoutes(router, userService, adjustmentService)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package adjustments
|
package adjustments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
|
||||||
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
|
|||||||
ctrl := controller.NewAdjustmentController(s)
|
ctrl := controller.NewAdjustmentController(s)
|
||||||
|
|
||||||
route := v1.Group("/adjustments")
|
route := v1.Group("/adjustments")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// Standard CRUD routes following master data pattern
|
// Standard CRUD routes following master data pattern
|
||||||
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
|
||||||
route.Post("/", ctrl.Adjustment) // Create adjustment
|
route.Post("/", ctrl.Adjustment) // Create adjustment
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
common "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"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
@@ -11,13 +16,10 @@ import (
|
|||||||
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdjustmentService interface {
|
type AdjustmentService interface {
|
||||||
@@ -27,22 +29,24 @@ type AdjustmentService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type adjustmentService struct {
|
type adjustmentService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
StockLogsRepository stockLogsRepo.StockLogRepository
|
StockLogsRepository stockLogsRepo.StockLogRepository
|
||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
|
||||||
ProductRepo productRepo.ProductRepository
|
ProductRepo productRepo.ProductRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
|
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService {
|
||||||
return &adjustmentService{
|
return &adjustmentService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
StockLogsRepository: stockLogsRepo,
|
StockLogsRepository: stockLogsRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
ProductRepo: productRepo,
|
ProductRepo: productRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +109,15 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
}
|
}
|
||||||
if !isProductWarehouseExist {
|
if !isProductWarehouseExist {
|
||||||
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
newPW := &entity.ProductWarehouse{
|
newPW := &entity.ProductWarehouse{
|
||||||
ProductId: uint(req.ProductID),
|
ProductId: uint(req.ProductID),
|
||||||
WarehouseId: uint(req.WarehouseID),
|
WarehouseId: uint(req.WarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
// CreatedBy: 1, // TODO: should Get from auth middleware
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
|
ctx,
|
||||||
|
uint(req.ProductID),
|
||||||
|
uint(req.WarehouseID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx,
|
||||||
|
s.StockLogsRepository.DB(),
|
||||||
|
[]uint{pw.Id},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,6 +195,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return s.GetOne(c, createdLogId)
|
return s.GetOne(c, createdLogId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
|
||||||
if err := s.Validate.Struct(query); err != nil {
|
if err := s.Validate.Struct(query); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
+4
-1
@@ -258,7 +258,10 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
|
|||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
Order("product_warehouses.id DESC").
|
Order("product_warehouses.id DESC").
|
||||||
Preload("Product").Preload("Warehouse").
|
Preload("Product").
|
||||||
|
Preload("Product.ProductCategory").
|
||||||
|
Preload("Product.Uom").
|
||||||
|
Preload("Warehouse").
|
||||||
Find(&productWarehouses).Error
|
Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
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"
|
||||||
@@ -25,8 +27,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
|
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
TransferRoutes(router, userService, transferService)
|
TransferRoutes(router, userService, transferService)
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
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"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,9 +40,11 @@ type transferService struct {
|
|||||||
StockLogsRepository rStockLogs.StockLogRepository
|
StockLogsRepository rStockLogs.StockLogRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
SupplierRepo rSupplier.SupplierRepository
|
SupplierRepo rSupplier.SupplierRepository
|
||||||
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
|
||||||
return &transferService{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -48,6 +55,8 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
StockLogsRepository: stockLogsRepo,
|
StockLogsRepository: stockLogsRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
SupplierRepo: supplierRepo,
|
SupplierRepo: supplierRepo,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
@@ -111,6 +120,8 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||||
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
|
pwIDs := make([]uint, 0, len(req.Products))
|
||||||
|
|
||||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
@@ -126,7 +137,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if sourcePW.Quantity < product.ProductQty {
|
if sourcePW.Quantity < product.ProductQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
||||||
}
|
}
|
||||||
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
s.StockTransferRepo.DB(),
|
||||||
|
pwIDs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -301,10 +321,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
// Jika belum ada record untuk produk di gudang tujuan, buat baru
|
||||||
|
ctx := c.Context()
|
||||||
|
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
destPW = &entity.ProductWarehouse{
|
destPW = &entity.ProductWarehouse{
|
||||||
ProductId: uint(product.ProductID),
|
ProductId: uint(product.ProductID),
|
||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
// CreatedBy: 1, // TODO: should Get from auth middleware
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
@@ -357,3 +383,29 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||||
|
}
|
||||||
|
|
||||||
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint(projectFlockKandang.Id), nil
|
||||||
|
}
|
||||||
|
|||||||
+6
-6
@@ -4,9 +4,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -23,7 +23,7 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.DeliveryOrderQuery{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
||||||
@@ -76,7 +76,7 @@ func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
req := new(validation.DeliveryOrderCreate)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
@@ -97,7 +97,7 @@ func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Update)
|
req := new(validation.DeliveryOrderUpdate)
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
+3
-3
@@ -3,9 +3,9 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package delivery_orderss
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
|
||||||
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeliveryOrdersModule struct{}
|
|
||||||
|
|
||||||
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
marketingRepo := rMarketing.NewMarketingRepository(db)
|
|
||||||
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
|
||||||
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
|
||||||
|
|
||||||
// Register workflow steps for MARKETINGS approval
|
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package delivery_orderss
|
|
||||||
|
|
||||||
import (
|
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
|
||||||
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
|
|
||||||
ctrl := controller.NewDeliveryOrdersController(s)
|
|
||||||
|
|
||||||
v1.Get("/", ctrl.GetAll)
|
|
||||||
v1.Get("/:id", ctrl.GetOne)
|
|
||||||
|
|
||||||
// Sisanya di group /delivery-orders
|
|
||||||
route := v1.Group("/delivery-orders")
|
|
||||||
route.Use(m.Auth(u))
|
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
|
|
||||||
}
|
|
||||||
+12
-11
@@ -24,7 +24,7 @@ type MarketingListDTO struct {
|
|||||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||||
SoDocs string `json:"so_docs"`
|
SoDocs string `json:"so_docs"`
|
||||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -36,13 +36,14 @@ type MarketingDetailDTO struct {
|
|||||||
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
Customer customerDTO.CustomerRelationDTO `json:"customer"`
|
||||||
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
|
||||||
SoDocs string `json:"so_docs"`
|
SoDocs string `json:"so_docs"`
|
||||||
SalesOrder []MarketingProductDTO `json:"sales_order"`
|
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
|
||||||
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
|
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
|
||||||
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingDeliveryProductDTO struct {
|
type MarketingDeliveryProductDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
MarketingProductId uint `json:"marketing_product_id"`
|
MarketingProductId uint `json:"marketing_product_id"`
|
||||||
@@ -73,7 +74,7 @@ type DeliveryGroupDTO struct {
|
|||||||
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
Deliveries []DeliveryItemDTO `json:"deliveries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingProductDTO struct {
|
type DeliveryMarketingProductDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
MarketingId uint `json:"marketing_id"`
|
MarketingId uint `json:"marketing_id"`
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
@@ -95,14 +96,14 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
|
func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO {
|
||||||
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
|
||||||
if e.ProductWarehouse.Id != 0 {
|
if e.ProductWarehouse.Id != 0 {
|
||||||
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
|
||||||
productWarehouse = &mapped
|
productWarehouse = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return MarketingProductDTO{
|
return DeliveryMarketingProductDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
MarketingId: e.MarketingId,
|
MarketingId: e.MarketingId,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
@@ -155,11 +156,11 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
|
|||||||
latestApproval = mapped
|
latestApproval = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var salesOrderProducts []MarketingProductDTO
|
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||||
if len(marketing.Products) > 0 {
|
if len(marketing.Products) > 0 {
|
||||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||||
for i, product := range marketing.Products {
|
for i, product := range marketing.Products {
|
||||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +196,11 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
|
|||||||
salesPerson = mapped
|
salesPerson = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var salesOrderProducts []MarketingProductDTO
|
var salesOrderProducts []DeliveryMarketingProductDTO
|
||||||
if len(marketing.Products) > 0 {
|
if len(marketing.Products) > 0 {
|
||||||
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
|
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
|
||||||
for i, product := range marketing.Products {
|
for i, product := range marketing.Products {
|
||||||
salesOrderProducts[i] = ToMarketingProductDTO(product)
|
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,52 @@
|
|||||||
package marketing
|
package marketing
|
||||||
|
|
||||||
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"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
|
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarketingModule struct{}
|
type MarketingModule struct{}
|
||||||
|
|
||||||
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
RegisterRoutes(router, db, validate)
|
// Initialize repositories
|
||||||
|
marketingRepo := repository.NewMarketingRepository(db)
|
||||||
|
marketingProductRepo := repository.NewMarketingProductRepository(db)
|
||||||
|
marketingDeliveryProductRepo := repository.NewMarketingDeliveryProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
|
||||||
|
// Initialize approval service
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
|
// Register workflow steps for marketing approval
|
||||||
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
// Initialize services
|
||||||
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
||||||
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarketingRepository interface {
|
||||||
|
repository.BaseRepository[entity.Marketing]
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
GetNextSequence(ctx context.Context) (uint, error)
|
||||||
|
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarketingRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Marketing]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
||||||
|
return &MarketingRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
||||||
|
var maxID uint
|
||||||
|
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return maxID + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||||
|
db := tx
|
||||||
|
if db == nil {
|
||||||
|
db = r.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
var soNumber string
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Raw("SELECT generate_so_number()").
|
||||||
|
Scan(&soNumber).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate SO number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return soNumber, nil
|
||||||
|
}
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
package marketing
|
package marketing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/controllers"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
|
|
||||||
deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss"
|
|
||||||
// MODULE IMPORTS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrdersService service.SalesOrdersService, deliveryOrdersService service.DeliveryOrdersService) {
|
||||||
group := router.Group("/marketing")
|
salesOrdersCtrl := controller.NewSalesOrdersController(salesOrdersService)
|
||||||
|
deliveryOrdersCtrl := controller.NewDeliveryOrdersController(deliveryOrdersService)
|
||||||
|
|
||||||
allModules := []modules.Module{
|
route := router.Group("/marketing")
|
||||||
salesOrderss.SalesOrdersModule{},
|
route.Use(m.Auth(userService))
|
||||||
deliveryOrderss.DeliveryOrdersModule{},
|
|
||||||
// MODULE REGISTRY
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range allModules {
|
route.Get("/", deliveryOrdersCtrl.GetAll)
|
||||||
m.RegisterRoutes(group, db, validate)
|
route.Get("/:id", deliveryOrdersCtrl.GetOne)
|
||||||
}
|
route.Delete("/:id", salesOrdersCtrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Post("/sales-orders", salesOrdersCtrl.CreateOne)
|
||||||
|
route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne)
|
||||||
|
route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval)
|
||||||
|
|
||||||
|
route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll)
|
||||||
|
route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne)
|
||||||
|
route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne)
|
||||||
|
route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package sales_orders
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
|
||||||
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
|
||||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SalesOrdersModule struct{}
|
|
||||||
|
|
||||||
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
|
||||||
marketingRepo := rSalesOrders.NewMarketingRepository(db)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
|
||||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
|
|
||||||
|
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate)
|
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
|
||||||
|
|
||||||
SalesOrdersRoutes(router, userService, salesOrdersService)
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingRepository interface {
|
|
||||||
repository.BaseRepository[entity.Marketing]
|
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
|
||||||
GetNextSequence(ctx context.Context) (uint, error)
|
|
||||||
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.Marketing]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
|
|
||||||
return &MarketingRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
|
|
||||||
var maxID uint
|
|
||||||
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package sales_orders
|
|
||||||
|
|
||||||
import (
|
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
|
||||||
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
|
|
||||||
ctrl := controller.NewSalesOrdersController(s)
|
|
||||||
|
|
||||||
v1.Delete("/:id", ctrl.DeleteOne)
|
|
||||||
route := v1.Group("/sales-orders")
|
|
||||||
route.Use(m.Auth(u))
|
|
||||||
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Post("/", ctrl.CreateOne)
|
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
|
|
||||||
route.Post("/approvals", ctrl.Approval)
|
|
||||||
}
|
|
||||||
+24
-9
@@ -11,9 +11,9 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -23,10 +23,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DeliveryOrdersService interface {
|
type DeliveryOrdersService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type deliveryOrdersService struct {
|
type deliveryOrdersService struct {
|
||||||
@@ -85,7 +85,7 @@ func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketin
|
|||||||
return &responseDTO, nil
|
return &responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) {
|
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDeta
|
|||||||
return &responseDTO, nil
|
return &responseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) {
|
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -222,6 +222,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -285,7 +293,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) {
|
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -319,6 +327,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
if foundMarketingProduct == nil {
|
if foundMarketingProduct == nil {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
+76
-17
@@ -11,9 +11,11 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/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"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -32,24 +34,29 @@ type SalesOrdersService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type salesOrdersService struct {
|
type salesOrdersService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo repository.MarketingRepository
|
MarketingRepo repository.MarketingRepository
|
||||||
CustomerRepo customerRepo.CustomerRepository
|
CustomerRepo customerRepo.CustomerRepository
|
||||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
UserRepo userRepo.UserRepository
|
UserRepo userRepo.UserRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService {
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
||||||
return &salesOrdersService{
|
return &salesOrdersService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
CustomerRepo: customerRepo,
|
CustomerRepo: customerRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +147,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(req.MarketingProducts) > 0 {
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
for _, product := range req.MarketingProducts {
|
for _, product := range req.MarketingProducts {
|
||||||
|
if product.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
||||||
|
}
|
||||||
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +228,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
|
for _, item := range req.MarketingProducts {
|
||||||
|
if item.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, item.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
@@ -367,7 +394,18 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||||
}
|
}
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
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 {
|
||||||
|
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
@@ -458,6 +496,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Products")
|
||||||
|
})
|
||||||
|
if mErr != nil {
|
||||||
|
if errors.Is(mErr, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id))
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
+4
-4
@@ -11,22 +11,22 @@ type DeliveryProduct struct {
|
|||||||
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Create struct {
|
type DeliveryOrderCreate struct {
|
||||||
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
|
||||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type DeliveryOrderUpdate struct {
|
||||||
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type DeliveryOrderQuery struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
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"`
|
||||||
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Approve struct {
|
type DeliveryOrderApprove struct {
|
||||||
Action string `json:"action" validate:"required_strict"`
|
Action string `json:"action" validate:"required_strict"`
|
||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -18,13 +18,14 @@ type NonstockRelationDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NonstockListDTO struct {
|
type NonstockListDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonstockDetailDTO struct {
|
type NonstockDetailDTO struct {
|
||||||
@@ -76,6 +77,7 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
|
|||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Uom: uomRef,
|
Uom: uomRef,
|
||||||
|
Suppliers: toNonstockSupplierDTOs(e.NonstockSuppliers),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
@@ -95,3 +97,23 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
|||||||
NonstockListDTO: ToNonstockListDTO(e),
|
NonstockListDTO: ToNonstockListDTO(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
||||||
|
if len(relations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||||
|
for _, relation := range relations {
|
||||||
|
if relation.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ func NewWarehouseController(warehouseService service.WarehouseService) *Warehous
|
|||||||
|
|
||||||
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
AreaId: c.QueryInt("area_id", 0),
|
AreaId: c.QueryInt("area_id", 0),
|
||||||
|
ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false),
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Page < 1 || query.Limit < 1 {
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
|||||||
@@ -53,11 +53,28 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
warehouses, 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+"%")
|
db = db.Where("warehouses.name LIKE ?", "%"+params.Search+"%")
|
||||||
}
|
}
|
||||||
if params.AreaId != 0 {
|
if params.AreaId != 0 {
|
||||||
db = db.Where("area_id = ?", params.AreaId)
|
db = db.Where("area_id = ?", params.AreaId)
|
||||||
}
|
}
|
||||||
|
if params.ActiveProjectFlockOnly {
|
||||||
|
db = db.Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM kandangs k
|
||||||
|
JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id
|
||||||
|
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 = pfk.project_flock_id
|
||||||
|
WHERE k.id = warehouses.kandang_id
|
||||||
|
AND LOWER(latest_approval.step_name) = LOWER(?)
|
||||||
|
)
|
||||||
|
`, "Aktif")
|
||||||
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||||
|
ActiveProjectFlockOnly bool `query:"active_project_flock"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type ProjectChickinRepository interface {
|
type ProjectChickinRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectChickin]
|
repository.BaseRepository[entity.ProjectChickin]
|
||||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error)
|
||||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
@@ -40,6 +41,16 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
|
|||||||
return &chickin, nil
|
return &chickin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error) {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Order("project_chickins.created_at DESC").
|
||||||
|
Find(&chickins).Error
|
||||||
|
return chickins, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
var chickins []entity.ProjectChickin
|
var chickins []entity.ProjectChickin
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -190,14 +190,14 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
}
|
}
|
||||||
|
|
||||||
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if category == string(utils.ProjectFlockCategoryLaying) {
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
for _, chickin := range newChikins {
|
for _, chickin := range newChikins {
|
||||||
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
|
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -218,9 +218,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
if latest == nil {
|
if latest == nil {
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
projectFlockKandang.Id,
|
projectFlockKandang.Id,
|
||||||
utils.ProjectFlockKandangStepPengajuan,
|
utils.ChickinStepPengajuan,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
@@ -228,12 +228,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
} else if latest.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
projectFlockKandang.Id,
|
projectFlockKandang.Id,
|
||||||
utils.ProjectFlockKandangStepPengajuan,
|
utils.ChickinStepPengajuan,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
@@ -388,7 +388,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
@@ -396,14 +396,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
if latestApproval == nil {
|
if latestApproval == nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
||||||
}
|
}
|
||||||
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
if latestApproval.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
step := utils.ProjectFlockKandangStepPengajuan
|
step := utils.ChickinStepPengajuan
|
||||||
if action == entity.ApprovalActionApproved {
|
if action == entity.ApprovalActionApproved {
|
||||||
step = utils.ProjectFlockKandangStepDisetujui
|
step = utils.ChickinStepDisetujui
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
@@ -415,7 +415,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
approvableID,
|
approvableID,
|
||||||
step,
|
step,
|
||||||
&action,
|
&action,
|
||||||
@@ -498,7 +498,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
for _, chickin := range chickins {
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
|
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -600,7 +600,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
|
|
||||||
if chickin.ProductWarehouseId != targetPW.Id {
|
if chickin.ProductWarehouseId != targetPW.Id {
|
||||||
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
||||||
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
|
"qty": gorm.Expr("qty - ?", quantityToConvert),
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
||||||
@@ -610,7 +610,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
||||||
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
|
"qty": gorm.Expr("qty + ?", quantityToConvert),
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
||||||
|
|||||||
+45
@@ -84,3 +84,48 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
|
|||||||
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
|
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.Closing)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ProjectFlockKandangService.Closing(c, uint(id), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Status closing kandang diperbarui",
|
||||||
|
// Data: dto.ProjectFlockKandangDetailDTO(*result),
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) CheckClosing(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ProjectFlockKandangService.CheckClosing(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Cek persyaratan closing kandang",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
|
|
||||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
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"
|
||||||
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -28,15 +29,17 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
|
|||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
// register workflow steps for project flock kandang approvals
|
// register workflow steps for chickin approvals
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
|
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||||
|
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||||
|
|||||||
@@ -22,5 +22,8 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
|
|||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
// route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing)
|
||||||
|
// route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing)
|
||||||
|
route.Post("/:id/closing", ctrl.Closing)
|
||||||
|
route.Get("/:id/closing/check", ctrl.CheckClosing)
|
||||||
}
|
}
|
||||||
|
|||||||
+351
-11
@@ -2,47 +2,84 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
"strings"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
"time"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockKandangService interface {
|
type ProjectFlockKandangService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
||||||
|
CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
|
||||||
|
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
|
||||||
|
GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity
|
|
||||||
|
|
||||||
type projectFlockKandangService struct {
|
type projectFlockKandangService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ProjectFlockKandangRepository
|
Repository repository.ProjectFlockKandangRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ExpenseRepo expenseRepo.ExpenseRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||||
|
KandangRepo kandangRepo.KandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
|
type ClosingCheckResult struct {
|
||||||
|
UnfinishedExpenses int64 `json:"unfinished_expenses"`
|
||||||
|
StockRemaining []StockRemainingDetail `json:"stock_remaining"`
|
||||||
|
Expenses []ExpenseSummary `json:"expenses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockRemainingDetail struct {
|
||||||
|
FlagName string `json:"flag_name"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
Uom string `json:"uom"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseSummary struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
PoNumber string `json:"po_number"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StepName string `json:"step_name"`
|
||||||
|
Step uint16 `json:"step"`
|
||||||
|
Reference string `json:"reference_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||||
return &projectFlockKandangService{
|
return &projectFlockKandangService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
ExpenseRepo: expenseRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
PopulationRepo: populationRepo,
|
PopulationRepo: populationRepo,
|
||||||
|
KandangRepo: kandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +203,309 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) {
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||||
|
count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||||
|
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
unfinished = count
|
||||||
|
}
|
||||||
|
|
||||||
|
stockRemain := make([]StockRemainingDetail, 0)
|
||||||
|
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||||
|
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||||
|
if werr != nil {
|
||||||
|
return nil, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||||
|
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||||
|
if pwErr != nil {
|
||||||
|
return nil, pwErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
if pw.Quantity > 0 {
|
||||||
|
category := ""
|
||||||
|
if pw.Product.ProductCategory.Id != 0 {
|
||||||
|
category = pw.Product.ProductCategory.Name
|
||||||
|
}
|
||||||
|
uomName := ""
|
||||||
|
if pw.Product.Uom.Id != 0 {
|
||||||
|
uomName = pw.Product.Uom.Name
|
||||||
|
}
|
||||||
|
stockRemain = append(stockRemain, StockRemainingDetail{
|
||||||
|
FlagName: string(flagName),
|
||||||
|
ProductWarehouseId: pw.Id,
|
||||||
|
ProductId: pw.ProductId,
|
||||||
|
ProductName: pw.Product.Name,
|
||||||
|
ProductCategory: category,
|
||||||
|
Uom: uomName,
|
||||||
|
Quantity: pw.Quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseSummaries := make([]ExpenseSummary, 0)
|
||||||
|
if s.ExpenseRepo != nil {
|
||||||
|
var expenses []entity.Expense
|
||||||
|
if err := s.ExpenseRepo.DB().WithContext(c.Context()).
|
||||||
|
Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)).
|
||||||
|
Preload("Nonstocks").
|
||||||
|
Find(&expenses).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expenses) > 0 && s.ApprovalSvc != nil {
|
||||||
|
ids := make([]uint, 0, len(expenses))
|
||||||
|
for _, e := range expenses {
|
||||||
|
ids = append(ids, uint(e.Id))
|
||||||
|
}
|
||||||
|
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil)
|
||||||
|
if err == nil {
|
||||||
|
for i := range expenses {
|
||||||
|
if latest, ok := latestMap[uint(expenses[i].Id)]; ok {
|
||||||
|
expenses[i].LatestApproval = latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range expenses {
|
||||||
|
total := 0.0
|
||||||
|
for _, ns := range exp.Nonstocks {
|
||||||
|
total += ns.Qty * ns.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "Pending"
|
||||||
|
stepName := ""
|
||||||
|
stepNum := uint16(0)
|
||||||
|
if exp.LatestApproval != nil {
|
||||||
|
stepName = exp.LatestApproval.StepName
|
||||||
|
stepNum = exp.LatestApproval.StepNumber
|
||||||
|
if exp.LatestApproval.Action != nil {
|
||||||
|
status = string(*exp.LatestApproval.Action)
|
||||||
|
} else if stepName != "" {
|
||||||
|
status = stepName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseSummaries = append(expenseSummaries, ExpenseSummary{
|
||||||
|
Id: exp.Id,
|
||||||
|
PoNumber: exp.PoNumber,
|
||||||
|
Category: exp.Category,
|
||||||
|
Total: total,
|
||||||
|
Status: status,
|
||||||
|
StepName: stepName,
|
||||||
|
Step: stepNum,
|
||||||
|
Reference: exp.ReferenceNumber,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ClosingCheckResult{
|
||||||
|
UnfinishedExpenses: unfinished,
|
||||||
|
StockRemaining: stockRemain,
|
||||||
|
Expenses: expenseSummaries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
|
||||||
|
func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pfk.ClosedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
latest, aerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||||
|
if aerr != nil {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||||
|
}
|
||||||
|
if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) && latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action := strings.ToLower(strings.TrimSpace(req.Action))
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "close":
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed")
|
||||||
|
}
|
||||||
|
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||||
|
unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||||
|
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if unfinished > 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Masih ada expense belum selesai untuk kandang ini")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||||
|
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||||
|
if werr != nil {
|
||||||
|
return nil, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||||
|
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||||
|
if pwErr != nil {
|
||||||
|
return nil, pwErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
if pw.Quantity > 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok %s masih tersedia (product warehouse %d: %.2f)", flagName, pw.Id, pw.Quantity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTime := now
|
||||||
|
if req.ClosedDate != nil {
|
||||||
|
parsed, perr := utils.ParseDateString(strings.TrimSpace(*req.ClosedDate))
|
||||||
|
if perr != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "closed_date tidak valid, gunakan format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
closeTime = parsed
|
||||||
|
}
|
||||||
|
if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.KandangRepo != nil {
|
||||||
|
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||||
|
c.Context(),
|
||||||
|
[]uint{pfk.KandangId},
|
||||||
|
utils.KandangStatusNonActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
closeAction := entity.ApprovalActionApproved
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
id,
|
||||||
|
utils.ProjectFlockKandangStepClosed,
|
||||||
|
&closeAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
|
||||||
|
pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId)
|
||||||
|
if ferr != nil {
|
||||||
|
return nil, ferr
|
||||||
|
}
|
||||||
|
allClosed := true
|
||||||
|
for _, item := range pfks {
|
||||||
|
if item.ClosedAt == nil {
|
||||||
|
allClosed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allClosed {
|
||||||
|
latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||||
|
if lerr != nil {
|
||||||
|
return nil, lerr
|
||||||
|
}
|
||||||
|
if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlock,
|
||||||
|
pfk.ProjectFlockId,
|
||||||
|
utils.ProjectFlockStepSelesai,
|
||||||
|
&closeAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "unclose":
|
||||||
|
if pfk.ClosedAt == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Kandang belum closed")
|
||||||
|
}
|
||||||
|
openNewer, err := s.Repository.HasOpenNewerPeriod(c.Context(), pfk.KandangId, pfk.Period, &pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if openNewer {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Tidak dapat un-close: ada periode yang sedang berjalan")
|
||||||
|
}
|
||||||
|
if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.KandangRepo != nil {
|
||||||
|
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||||
|
c.Context(),
|
||||||
|
[]uint{pfk.KandangId},
|
||||||
|
utils.KandangStatusActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Repository.GetByID(c.Context(), id)
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||||
availableQty := productWarehouse.Quantity
|
availableQty := productWarehouse.Quantity
|
||||||
|
|
||||||
|
|||||||
+5
@@ -22,3 +22,8 @@ type Query struct {
|
|||||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
||||||
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Closing struct {
|
||||||
|
Action string `json:"action" validate:"required,oneof=close unclose"`
|
||||||
|
ClosedDate *string `json:"closed_date,omitempty"`
|
||||||
|
}
|
||||||
@@ -165,33 +165,6 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ProjectflockService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Update projectflock successfully",
|
|
||||||
Data: dto.ToProjectFlockListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
|
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -8,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type ProjectBudgetRepository interface {
|
type ProjectBudgetRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectBudget]
|
repository.BaseRepository[entity.ProjectBudget]
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectBudgetRepositoryImpl struct {
|
type ProjectBudgetRepositoryImpl struct {
|
||||||
@@ -21,3 +24,13 @@ func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
|
|||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error) {
|
||||||
|
var budgets []entity.ProjectBudget
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Preload("Nonstock").
|
||||||
|
Preload("Nonstock.Uom").
|
||||||
|
Find(&budgets).Error
|
||||||
|
return budgets, err
|
||||||
|
}
|
||||||
|
|||||||
+60
-21
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -14,18 +15,20 @@ 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)
|
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||||
|
UpdateClosedAt(ctx context.Context, id uint, t *time.Time) 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)
|
||||||
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
|
||||||
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||||
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
||||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
|
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
DB() *gorm.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectFlockKandangRepositoryImpl struct {
|
type projectFlockKandangRepositoryImpl struct {
|
||||||
@@ -75,6 +78,16 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i
|
|||||||
return records, total, nil
|
return records, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
var records []entity.ProjectFlockKandang
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
||||||
var records []entity.ProjectFlockKandang
|
var records []entity.ProjectFlockKandang
|
||||||
var total int64
|
var total int64
|
||||||
@@ -223,32 +236,57 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||||
record := new(entity.ProjectFlockKandang)
|
latestApprovalSubQuery := r.db.
|
||||||
|
Table("approvals").
|
||||||
|
Select("DISTINCT ON (approvable_id) approvable_id, step_name, action_at").
|
||||||
|
Where("approvable_type = ?", "PROJECT_FLOCKS").
|
||||||
|
Order("approvable_id, action_at DESC")
|
||||||
|
|
||||||
|
var pfkID uint
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||||
Joins(`
|
Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
|
||||||
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("project_flock_kandangs.kandang_id = ?", kandangID).
|
||||||
|
Where("project_flock_kandangs.closed_at IS NULL").
|
||||||
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
||||||
Order("project_flock_kandangs.id DESC").
|
Order("project_flock_kandangs.id DESC").
|
||||||
Preload("ProjectFlock").
|
Limit(1).
|
||||||
Preload("ProjectFlock.Fcr").
|
Pluck("project_flock_kandangs.id", &pfkID).Error; err != nil {
|
||||||
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 nil, err
|
||||||
}
|
}
|
||||||
return record, nil
|
|
||||||
|
if pfkID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetByID(ctx, pfkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("closed_at", t).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) {
|
||||||
|
if kandangID == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
q := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("kandang_id = ?", kandangID).
|
||||||
|
Where("period > ?", currentPeriod).
|
||||||
|
Where("closed_at IS NULL")
|
||||||
|
if excludeID != nil {
|
||||||
|
q = q.Where("id <> ?", *excludeID)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := q.Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, 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) {
|
||||||
@@ -269,7 +307,8 @@ func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx
|
|||||||
}
|
}
|
||||||
q := r.db.WithContext(ctx).
|
q := r.db.WithContext(ctx).
|
||||||
Table("project_flock_kandangs").
|
Table("project_flock_kandangs").
|
||||||
Where("kandang_id IN ?", kandangIDs)
|
Where("kandang_id IN ?", kandangIDs).
|
||||||
|
Where("closed_at IS NULL")
|
||||||
if exceptProjectID != nil {
|
if exceptProjectID != nil {
|
||||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type ProjectflockService interface {
|
|||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
|
|
||||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||||
@@ -255,6 +254,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var location entity.Location
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Where("id = ? AND area_id = ?", req.LocationId, req.AreaId).
|
||||||
|
First(&location).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi")
|
||||||
|
}
|
||||||
|
|
||||||
canonicalBase := baseName
|
canonicalBase := baseName
|
||||||
if s.FlockRepo != nil {
|
if s.FlockRepo != nil {
|
||||||
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
||||||
@@ -348,365 +357,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return s.getOneEntityOnly(c, createBody.Id)
|
return s.getOneEntityOnly(c, createBody.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
updateBody := make(map[string]any)
|
|
||||||
hasBodyChanges := false
|
|
||||||
var relationChecks []commonSvc.RelationCheck
|
|
||||||
existingBase := pfutils.DeriveBaseName(existing.FlockName)
|
|
||||||
targetBaseName := existingBase
|
|
||||||
needFlockNameRegenerate := false
|
|
||||||
|
|
||||||
if req.FlockName != nil {
|
|
||||||
trimmed := strings.TrimSpace(*req.FlockName)
|
|
||||||
if trimmed == "" {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
|
||||||
}
|
|
||||||
canonicalBase := trimmed
|
|
||||||
if s.FlockRepo != nil {
|
|
||||||
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
canonicalBase = flockEntity.Name
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(canonicalBase, existingBase) {
|
|
||||||
needFlockNameRegenerate = true
|
|
||||||
targetBaseName = canonicalBase
|
|
||||||
hasBodyChanges = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.AreaId != nil {
|
|
||||||
updateBody["area_id"] = *req.AreaId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "Area",
|
|
||||||
ID: req.AreaId,
|
|
||||||
Exists: s.Repository.AreaExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.Category != nil {
|
|
||||||
cat := strings.ToUpper(*req.Category)
|
|
||||||
if !utils.IsValidProjectFlockCategory(cat) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBody["category"] = cat
|
|
||||||
}
|
|
||||||
if req.FcrId != nil {
|
|
||||||
updateBody["fcr_id"] = *req.FcrId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "FCR",
|
|
||||||
ID: req.FcrId,
|
|
||||||
Exists: s.Repository.FcrExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.LocationId != nil {
|
|
||||||
updateBody["location_id"] = *req.LocationId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "Location",
|
|
||||||
ID: req.LocationId,
|
|
||||||
Exists: s.Repository.LocationExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(relationChecks) > 0 {
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newKandangIDs []uint
|
|
||||||
hasKandangChanges := false
|
|
||||||
if req.KandangIds != nil {
|
|
||||||
hasKandangChanges = true
|
|
||||||
if len(req.KandangIds) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
|
|
||||||
}
|
|
||||||
newKandangIDs = uniqueUintSlice(req.KandangIds)
|
|
||||||
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, nil)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
|
||||||
}
|
|
||||||
if len(kandangs) != len(newKandangIDs) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
|
||||||
}
|
|
||||||
targetLocationID := existing.LocationId
|
|
||||||
if req.LocationId != nil && *req.LocationId > 0 {
|
|
||||||
targetLocationID = *req.LocationId
|
|
||||||
}
|
|
||||||
for _, kandang := range kandangs {
|
|
||||||
if kandang.LocationId != targetLocationID {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
|
||||||
} else if linked {
|
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hasChanges := hasBodyChanges || hasKandangChanges
|
|
||||||
if !hasChanges {
|
|
||||||
return s.getOneEntityOnly(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
|
||||||
|
|
||||||
baseForGeneration := targetBaseName
|
|
||||||
if strings.TrimSpace(baseForGeneration) == "" {
|
|
||||||
baseForGeneration = existingBase
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(baseForGeneration) == "" {
|
|
||||||
baseForGeneration = strings.TrimSpace(existing.FlockName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if needFlockNameRegenerate {
|
|
||||||
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updateBody["flock_name"] = newName
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updateBody) > 0 {
|
|
||||||
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.KandangIds != nil {
|
|
||||||
existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
|
|
||||||
for _, k := range existing.Kandangs {
|
|
||||||
existingIDs[k.Id] = struct{}{}
|
|
||||||
}
|
|
||||||
newSet := make(map[uint]struct{}, len(newKandangIDs))
|
|
||||||
for _, kid := range newKandangIDs {
|
|
||||||
newSet[kid] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var toDetach []uint
|
|
||||||
for kid := range existingIDs {
|
|
||||||
if _, ok := newSet[kid]; !ok {
|
|
||||||
toDetach = append(toDetach, kid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var toAttach []uint
|
|
||||||
for kid := range newSet {
|
|
||||||
if _, ok := existingIDs[kid]; !ok {
|
|
||||||
toAttach = append(toAttach, kid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(toDetach) > 0 {
|
|
||||||
if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(toAttach) > 0 {
|
|
||||||
currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
periods := make(map[uint]int, len(toAttach))
|
|
||||||
if currentPeriod > 0 {
|
|
||||||
for _, kid := range toAttach {
|
|
||||||
periods[kid] = currentPeriod
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasChanges {
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
if approvalSvc != nil {
|
|
||||||
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
shouldRecordUpdate := latestBeforeReset == nil ||
|
|
||||||
latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
|
|
||||||
latestBeforeReset.Action == nil ||
|
|
||||||
(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)
|
|
||||||
|
|
||||||
if shouldRecordUpdate {
|
|
||||||
action := entity.ApprovalActionUpdated
|
|
||||||
if _, err := approvalSvc.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
utils.ApprovalWorkflowProjectFlock,
|
|
||||||
id,
|
|
||||||
utils.ProjectFlockStepPengajuan,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
nil,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
||||||
return nil, fiberErr
|
|
||||||
}
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
|
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.getOneEntityOnly(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var action entity.ApprovalAction
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
|
||||||
case string(entity.ApprovalActionRejected):
|
|
||||||
action = entity.ApprovalActionRejected
|
|
||||||
case string(entity.ApprovalActionApproved):
|
|
||||||
action = entity.ApprovalActionApproved
|
|
||||||
default:
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
|
||||||
}
|
|
||||||
|
|
||||||
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
|
||||||
if len(approvableIDs) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
|
||||||
}
|
|
||||||
|
|
||||||
step := utils.ProjectFlockStepPengajuan
|
|
||||||
if action == entity.ApprovalActionApproved {
|
|
||||||
step = utils.ProjectFlockStepAktif
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
|
||||||
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
|
||||||
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := approvalSvc.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
utils.ApprovalWorkflowProjectFlock,
|
|
||||||
approvableID,
|
|
||||||
step,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
req.Notes,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case entity.ApprovalActionApproved:
|
|
||||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
|
||||||
c.Context(),
|
|
||||||
approvableID,
|
|
||||||
utils.KandangStatusActive,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case entity.ApprovalActionRejected:
|
|
||||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
|
||||||
c.Context(),
|
|
||||||
approvableID,
|
|
||||||
utils.KandangStatusNonActive,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
||||||
return nil, fiberErr
|
|
||||||
}
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
|
||||||
}
|
|
||||||
|
|
||||||
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
|
||||||
for _, approvableID := range approvableIDs {
|
|
||||||
project, err := s.getOneEntityOnly(c, approvableID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
updated = append(updated, *project)
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -827,6 +477,27 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getProjectFlockClosingDate mengembalikan tanggal closing Project Flock jika sudah mencapai step SELESAI (Approved).
|
||||||
|
// func (s projectflockService) getProjectFlockClosingDate(ctx context.Context, projectFlockID uint) (*time.Time, error) {
|
||||||
|
// if projectFlockID == 0 || s.ApprovalSvc == nil {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowProjectFlock, projectFlockID, nil)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
// if latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// t := latest.ActionAt
|
||||||
|
// return &t, nil
|
||||||
|
// }
|
||||||
|
|
||||||
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
||||||
if len(projectIDs) == 0 {
|
if len(projectIDs) == 0 {
|
||||||
return map[uint]int{}, nil
|
return map[uint]int{}, nil
|
||||||
@@ -834,6 +505,133 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint)
|
|||||||
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
step := utils.ProjectFlockStepPengajuan
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
step = utils.ProjectFlockStepAktif
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
||||||
|
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
||||||
|
projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlock,
|
||||||
|
approvableID,
|
||||||
|
step,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case entity.ApprovalActionApproved:
|
||||||
|
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||||
|
c.Context(),
|
||||||
|
approvableID,
|
||||||
|
utils.KandangStatusActive,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, pfk := range pfks {
|
||||||
|
latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil)
|
||||||
|
if lerr != nil {
|
||||||
|
return lerr
|
||||||
|
}
|
||||||
|
if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, aerr := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
pfk.Id,
|
||||||
|
utils.ProjectFlockKandangStepDisetujui,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||||
|
return aerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case entity.ApprovalActionRejected:
|
||||||
|
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||||
|
c.Context(),
|
||||||
|
approvableID,
|
||||||
|
utils.KandangStatusNonActive,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
project, err := s.getOneEntityOnly(c, approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updated = append(updated, *project)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
||||||
if locationID == 0 {
|
if locationID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ type Create struct {
|
|||||||
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
|
||||||
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
|
|
||||||
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
Category *string `json:"category,omitempty" validate:"omitempty"`
|
|
||||||
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
|
|||||||
@@ -146,27 +146,6 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.SubmitGrading)
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.RecordingService.SubmitGrading(c, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Submit grading eggs successfully",
|
|
||||||
Data: dto.ToRecordingDetailDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *RecordingController) Approve(c *fiber.Ctx) error {
|
func (u *RecordingController) Approve(c *fiber.Ctx) error {
|
||||||
req := new(validation.Approve)
|
req := new(validation.Approve)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,22 +15,19 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type RecordingRelationDTO struct {
|
type RecordingRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
RecordDatetime time.Time `json:"record_datetime"`
|
RecordDatetime time.Time `json:"record_datetime"`
|
||||||
Day int `json:"day"`
|
Day int `json:"day"`
|
||||||
ProjectFlockCategory string `json:"project_flock_category"`
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||||
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||||
DailyGain float64 `json:"daily_gain"`
|
DailyGain float64 `json:"daily_gain"`
|
||||||
AvgDailyGain float64 `json:"avg_daily_gain"`
|
AvgDailyGain float64 `json:"avg_daily_gain"`
|
||||||
CumIntake int `json:"cum_intake"`
|
CumIntake int `json:"cum_intake"`
|
||||||
FcrValue float64 `json:"fcr_value"`
|
FcrValue float64 `json:"fcr_value"`
|
||||||
TotalChickQty float64 `json:"total_chick_qty"`
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
EggGradingStatus *string `json:"egg_grading_status"`
|
|
||||||
EggGradingPendingQty *int `json:"egg_grading_pending_qty"`
|
|
||||||
EggGradingCompletedQty *int `json:"egg_grading_completed_qty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingListDTO struct {
|
type RecordingListDTO struct {
|
||||||
@@ -72,8 +68,8 @@ type RecordingEggDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Qty int `json:"qty"`
|
Qty int `json:"qty"`
|
||||||
|
Weight *float64 `json:"weight,omitempty"`
|
||||||
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingProductWarehouseDTO struct {
|
type RecordingProductWarehouseDTO struct {
|
||||||
@@ -84,11 +80,6 @@ type RecordingProductWarehouseDTO struct {
|
|||||||
WarehouseName string `json:"warehouse_name"`
|
WarehouseName string `json:"warehouse_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingEggGradingDTO struct {
|
|
||||||
Grade string `json:"grade,omitempty"`
|
|
||||||
Qty float64 `json:"qty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||||
@@ -140,25 +131,20 @@ func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
|||||||
latestApproval = snapshot
|
latestApproval = snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
|
|
||||||
|
|
||||||
return RecordingRelationDTO{
|
return RecordingRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
RecordDatetime: e.RecordDatetime,
|
RecordDatetime: e.RecordDatetime,
|
||||||
Day: day,
|
Day: day,
|
||||||
ProjectFlockCategory: projectFlockCategory,
|
ProjectFlockCategory: projectFlockCategory,
|
||||||
TotalDepletionQty: totalDepletionQty,
|
TotalDepletionQty: totalDepletionQty,
|
||||||
CumDepletionRate: cumDepletionRate,
|
CumDepletionRate: cumDepletionRate,
|
||||||
DailyGain: dailyGain,
|
DailyGain: dailyGain,
|
||||||
AvgDailyGain: avgDailyGain,
|
AvgDailyGain: avgDailyGain,
|
||||||
CumIntake: cumIntake,
|
CumIntake: cumIntake,
|
||||||
FcrValue: fcrValue,
|
FcrValue: fcrValue,
|
||||||
TotalChickQty: totalChickQty,
|
TotalChickQty: totalChickQty,
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
EggGradingStatus: gradingStatus,
|
|
||||||
EggGradingPendingQty: gradingPending,
|
|
||||||
EggGradingCompletedQty: gradingCompleted,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,29 +239,13 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
|||||||
Id: egg.Id,
|
Id: egg.Id,
|
||||||
ProductWarehouseId: egg.ProductWarehouseId,
|
ProductWarehouseId: egg.ProductWarehouseId,
|
||||||
Qty: egg.Qty,
|
Qty: egg.Qty,
|
||||||
|
Weight: egg.Weight,
|
||||||
ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse),
|
||||||
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
|
|
||||||
if len(gradings) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]RecordingEggGradingDTO, len(gradings))
|
|
||||||
for i, grading := range gradings {
|
|
||||||
result[i] = RecordingEggGradingDTO{
|
|
||||||
Grade: grading.Grade,
|
|
||||||
Qty: grading.Qty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
||||||
if pw == nil {
|
if pw == nil {
|
||||||
return productWarehouseDTO.ProductWarehouseDTO{}
|
return productWarehouseDTO.ProductWarehouseDTO{}
|
||||||
@@ -289,61 +259,6 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro
|
|||||||
return *mapped
|
return *mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
const goodEggProductWarehouseID uint = 5
|
|
||||||
|
|
||||||
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
|
|
||||||
goodEggs := filterGoodEggs(e.Eggs)
|
|
||||||
if len(goodEggs) == 0 {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalEggs := 0
|
|
||||||
totalGraded := 0.0
|
|
||||||
for _, egg := range goodEggs {
|
|
||||||
totalEggs += egg.Qty
|
|
||||||
for _, grading := range egg.GradingEggs {
|
|
||||||
totalGraded += grading.Qty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalEggs == 0 {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingFloat := float64(totalEggs) - totalGraded
|
|
||||||
if pendingFloat < 0 {
|
|
||||||
pendingFloat = 0
|
|
||||||
}
|
|
||||||
pendingInt := int(math.Round(pendingFloat))
|
|
||||||
completedInt := int(math.Round(totalGraded))
|
|
||||||
if completedInt < 0 {
|
|
||||||
completedInt = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if pendingInt > 0 {
|
|
||||||
status := "GRADING_TELUR"
|
|
||||||
return &status, &pendingInt, &completedInt
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "GRADING_SELESAI"
|
|
||||||
zero := 0
|
|
||||||
return &status, &zero, &completedInt
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
|
|
||||||
if len(eggs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]entity.RecordingEgg, 0, len(eggs))
|
|
||||||
for _, egg := range eggs {
|
|
||||||
if egg.ProductWarehouseId == goodEggProductWarehouseID {
|
|
||||||
result = append(result, egg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO {
|
||||||
result := approvalDTO.ApprovalRelationDTO{}
|
result := approvalDTO.ApprovalRelationDTO{}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
ProductWarehouseID: "product_warehouse_id",
|
ProductWarehouseID: "product_warehouse_id",
|
||||||
UsageQuantity: "usage_qty",
|
UsageQuantity: "usage_qty",
|
||||||
PendingQuantity: "pending_qty",
|
PendingQuantity: "pending_qty",
|
||||||
CreatedAt: "created_at",
|
CreatedAt: "id",
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package recordings
|
|
||||||
|
|
||||||
const (
|
|
||||||
PermissionRecordingRead = "recording.read"
|
|
||||||
PermissionRecordingCreate = "recording.write"
|
|
||||||
PermissionRecordingUpdate = "recording.update"
|
|
||||||
PermissionRecordingDelete = "recording.delete"
|
|
||||||
)
|
|
||||||
@@ -35,8 +35,6 @@ type RecordingRepository interface {
|
|||||||
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
||||||
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
||||||
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
||||||
CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error
|
|
||||||
DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error
|
|
||||||
|
|
||||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||||
|
|
||||||
@@ -76,8 +74,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Eggs").
|
Preload("Eggs").
|
||||||
Preload("Eggs.ProductWarehouse").
|
Preload("Eggs.ProductWarehouse").
|
||||||
Preload("Eggs.ProductWarehouse.Product").
|
Preload("Eggs.ProductWarehouse.Product").
|
||||||
Preload("Eggs.ProductWarehouse.Warehouse").
|
Preload("Eggs.ProductWarehouse.Warehouse")
|
||||||
Preload("Eggs.GradingEggs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||||
@@ -188,7 +185,6 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
|||||||
Preload("Recording.ProjectFlockKandang").
|
Preload("Recording.ProjectFlockKandang").
|
||||||
Preload("Recording.ProjectFlockKandang.ProjectFlock").
|
Preload("Recording.ProjectFlockKandang.ProjectFlock").
|
||||||
Preload("ProductWarehouse").
|
Preload("ProductWarehouse").
|
||||||
Preload("GradingEggs").
|
|
||||||
Where("id = ?", id)
|
Where("id = ?", id)
|
||||||
|
|
||||||
if err := query.First(&egg).Error; err != nil {
|
if err := query.First(&egg).Error; err != nil {
|
||||||
@@ -197,17 +193,6 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
|||||||
return &egg, nil
|
return &egg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error {
|
|
||||||
if len(gradings) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tx.Create(&gradings).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error {
|
|
||||||
return tx.Where("recording_egg_id = ?", recordingEggID).Delete(&entity.GradingEgg{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
|
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
|
||||||
if projectFlockKandangId == 0 {
|
if projectFlockKandangId == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
|||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/next-day", ctrl.GetNextDay)
|
route.Get("/next-day", ctrl.GetNextDay)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Post("/gradings", ctrl.SubmitGrading)
|
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Post("/approvals", ctrl.Approve)
|
route.Post("/approvals", ctrl.Approve)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type RecordingService interface {
|
|||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
SubmitGrading(ctx *fiber.Ctx, req *validation.SubmitGrading) (*entity.Recording, error)
|
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +272,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
action := entity.ApprovalActionCreated
|
action := entity.ApprovalActionCreated
|
||||||
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepGradingTelur, action, createdRecording.CreatedBy, nil); err != nil {
|
if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create recording approval for %d: %+v", createdRecording.Id, err)
|
s.Log.Errorf("Failed to create recording approval for %d: %+v", createdRecording.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -347,16 +346,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasExistingGradings := false
|
|
||||||
for _, egg := range recordingEntity.Eggs {
|
|
||||||
if len(egg.GradingEggs) > 0 {
|
|
||||||
hasExistingGradings = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
|
|
||||||
|
|
||||||
if hasBodyChanges {
|
if hasBodyChanges {
|
||||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||||
@@ -441,9 +430,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasExistingGradings = false
|
|
||||||
hasEggsAfterUpdate = len(req.Eggs) > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
||||||
@@ -459,20 +445,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
|
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
var step approvalutils.ApprovalStep
|
step := utils.RecordingStepPengajuan
|
||||||
if isLaying {
|
|
||||||
if !hasEggsAfterUpdate {
|
|
||||||
step = utils.RecordingStepGradingTelur
|
|
||||||
} else if hasEggChanges {
|
|
||||||
step = utils.RecordingStepGradingTelur
|
|
||||||
} else if hasExistingGradings {
|
|
||||||
step = utils.RecordingStepPengajuan
|
|
||||||
} else {
|
|
||||||
step = utils.RecordingStepGradingTelur
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
step = utils.RecordingStepPengajuan
|
|
||||||
}
|
|
||||||
|
|
||||||
latestApproval := recordingEntity.LatestApproval
|
latestApproval := recordingEntity.LatestApproval
|
||||||
if latestApproval == nil {
|
if latestApproval == nil {
|
||||||
@@ -517,109 +490,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) SubmitGrading(c *fiber.Ctx, req *validation.SubmitGrading) (*entity.Recording, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.EggsGrading) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "eggs_grading must contain at least one item")
|
|
||||||
}
|
|
||||||
|
|
||||||
recordingEggID := req.EggsGrading[0].RecordingEggId
|
|
||||||
for _, grading := range req.EggsGrading[1:] {
|
|
||||||
if grading.RecordingEggId != recordingEggID {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "semua grading harus untuk recording egg yang sama")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := c.Context()
|
|
||||||
var recordingID uint
|
|
||||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
recordingEgg, err := s.Repository.GetRecordingEggByID(ctx, recordingEggID, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return tx
|
|
||||||
})
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Recording egg not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get recording egg %d: %+v", recordingEggID, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var category string
|
|
||||||
if recordingEgg.Recording.ProjectFlockKandang != nil && recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
|
||||||
category = strings.ToUpper(recordingEgg.Recording.ProjectFlockKandang.ProjectFlock.Category)
|
|
||||||
}
|
|
||||||
if category != strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Grading eggs hanya diperbolehkan pada project flock dengan kategori laying")
|
|
||||||
}
|
|
||||||
|
|
||||||
totalGradingQty := 0.0
|
|
||||||
for _, grading := range req.EggsGrading {
|
|
||||||
totalGradingQty += grading.Qty
|
|
||||||
}
|
|
||||||
|
|
||||||
availableRecorded := float64(recordingEgg.Qty)
|
|
||||||
if totalGradingQty > availableRecorded {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Total grading (%.2f) melebihi jumlah telur tercatat (%.2f)", totalGradingQty, availableRecorded),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordingEgg.ProductWarehouse.Id != 0 {
|
|
||||||
availableWarehouse := recordingEgg.ProductWarehouse.Quantity
|
|
||||||
if totalGradingQty > availableWarehouse {
|
|
||||||
return fiber.NewError(
|
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Total grading (%.2f) melebihi stok telur baik (%.2f)", totalGradingQty, availableWarehouse),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteGradingEggs(tx, recordingEgg.Id); err != nil {
|
|
||||||
s.Log.Errorf("Failed to clear grading eggs for recording egg %d: %+v", recordingEgg.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gradings := make([]entity.GradingEgg, 0, len(req.EggsGrading))
|
|
||||||
createdBy := recordingEgg.CreatedBy
|
|
||||||
if createdBy == 0 {
|
|
||||||
createdBy = recordingEgg.Recording.CreatedBy
|
|
||||||
}
|
|
||||||
for _, item := range req.EggsGrading {
|
|
||||||
gradings = append(gradings, entity.GradingEgg{
|
|
||||||
RecordingEggId: recordingEgg.Id,
|
|
||||||
Grade: strings.TrimSpace(item.Grade),
|
|
||||||
Qty: item.Qty,
|
|
||||||
CreatedBy: createdBy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(gradings) > 0 {
|
|
||||||
if err := s.Repository.CreateGradingEggs(tx, gradings); err != nil {
|
|
||||||
s.Log.Errorf("Failed to persist grading eggs for recording egg %d: %+v", recordingEgg.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action := entity.ApprovalActionUpdated
|
|
||||||
if err := s.createRecordingApproval(ctx, tx, recordingEgg.RecordingId, utils.RecordingStepPengajuan, action, createdBy, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to create approval after grading for recording %d: %+v", recordingEgg.RecordingId, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
recordingID = recordingEgg.RecordingId
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if transactionErr != nil {
|
|
||||||
return nil, transactionErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.GetOne(c, recordingID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -934,14 +804,10 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fcrId, err := s.Repository.GetFcrID(tx, recording.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getFcrID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAvgGrams := recordingutil.ToGrams(currentAvgWeight)
|
currentAvgGrams := recordingutil.ToGrams(currentAvgWeight)
|
||||||
currentAvgKg := recordingutil.GramsToKg(currentAvgGrams)
|
currentAvgKg := recordingutil.GramsToKg(currentAvgGrams)
|
||||||
prevAvgGrams := recordingutil.ToGrams(prevAvgWeight)
|
prevAvgGrams := recordingutil.ToGrams(prevAvgWeight)
|
||||||
|
prevAvgKg := recordingutil.GramsToKg(prevAvgGrams)
|
||||||
|
|
||||||
currentDepletion := float64(totalDepletionQty)
|
currentDepletion := float64(totalDepletionQty)
|
||||||
cumDepletionQty := prevCumDepletionQty + currentDepletion
|
cumDepletionQty := prevCumDepletionQty + currentDepletion
|
||||||
@@ -951,9 +817,10 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
}
|
}
|
||||||
recording.TotalDepletionQty = &cumDepletionQty
|
recording.TotalDepletionQty = &cumDepletionQty
|
||||||
|
|
||||||
|
var remainingChick float64
|
||||||
if totalChick > 0 {
|
if totalChick > 0 {
|
||||||
totalChickFloat := float64(totalChick)
|
totalChickFloat := float64(totalChick)
|
||||||
remainingChick := totalChickFloat - cumDepletionQty
|
remainingChick = totalChickFloat - cumDepletionQty
|
||||||
if remainingChick < 0 {
|
if remainingChick < 0 {
|
||||||
remainingChick = 0
|
remainingChick = 0
|
||||||
}
|
}
|
||||||
@@ -978,24 +845,19 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
|||||||
updates["daily_gain"] = dailyGainKg
|
updates["daily_gain"] = dailyGainKg
|
||||||
recording.DailyGain = &dailyGainKg
|
recording.DailyGain = &dailyGainKg
|
||||||
} else {
|
} else {
|
||||||
updates["daily_gain"] = gorm.Expr("NULL")
|
dailyGainKg := 0.0
|
||||||
recording.DailyGain = nil
|
updates["daily_gain"] = dailyGainKg
|
||||||
|
recording.DailyGain = &dailyGainKg
|
||||||
}
|
}
|
||||||
|
|
||||||
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
if currentAvgKg > 0 && remainingChick > 0 {
|
||||||
if fcrWeightKg, ok, err := s.Repository.GetFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
avgDailyGain := (currentAvgKg - prevAvgKg) / remainingChick
|
||||||
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
updates["avg_daily_gain"] = avgDailyGain
|
||||||
} else if ok {
|
recording.AvgDailyGain = &avgDailyGain
|
||||||
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
|
||||||
updates["avg_daily_gain"] = avgDailyGain
|
|
||||||
recording.AvgDailyGain = &avgDailyGain
|
|
||||||
} else {
|
|
||||||
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
|
||||||
recording.AvgDailyGain = nil
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
avgDailyGain := 0.0
|
||||||
recording.AvgDailyGain = nil
|
updates["avg_daily_gain"] = avgDailyGain
|
||||||
|
recording.AvgDailyGain = &avgDailyGain
|
||||||
}
|
}
|
||||||
|
|
||||||
if usageInGrams > 0 && totalChick > 0 {
|
if usageInGrams > 0 && totalChick > 0 {
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
Egg struct {
|
Egg struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
Qty int `json:"qty" validate:"required,number,min=0"`
|
Qty int `json:"qty" validate:"required,number,min=0"`
|
||||||
|
Weight *float64 `json:"weight,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,16 +46,6 @@ type Query struct {
|
|||||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EggGrading struct {
|
|
||||||
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
|
|
||||||
Grade string `json:"grade" validate:"required"`
|
|
||||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubmitGrading struct {
|
|
||||||
EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Approve struct {
|
type Approve struct {
|
||||||
Action string `json:"action" validate:"required_strict"`
|
Action string `json:"action" validate:"required_strict"`
|
||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseRelationDTO struct {
|
type PurchaseRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
PrNumber string `json:"pr_number"`
|
PrNumber string `json:"pr_number"`
|
||||||
PoNumber *string `json:"po_number"`
|
PoNumber *string `json:"po_number"`
|
||||||
PoDate *time.Time `json:"po_date"`
|
PoDate *time.Time `json:"po_date"`
|
||||||
Notes *string `json:"notes"`
|
CreditTerm int `json:"credit_term"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type PurchaseListDTO struct {
|
type PurchaseListDTO struct {
|
||||||
PurchaseRelationDTO
|
PurchaseRelationDTO
|
||||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||||
CreditTerm *int `json:"credit_term"`
|
|
||||||
DueDate *time.Time `json:"due_date"`
|
DueDate *time.Time `json:"due_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -37,9 +35,7 @@ type PurchaseListDTO struct {
|
|||||||
type PurchaseDetailDTO struct {
|
type PurchaseDetailDTO struct {
|
||||||
PurchaseRelationDTO
|
PurchaseRelationDTO
|
||||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||||
CreditTerm *int `json:"credit_term"`
|
|
||||||
DueDate *time.Time `json:"due_date"`
|
DueDate *time.Time `json:"due_date"`
|
||||||
GrandTotal float64 `json:"grand_total"`
|
|
||||||
Items []PurchaseItemDTO `json:"items"`
|
Items []PurchaseItemDTO `json:"items"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
@@ -47,7 +43,6 @@ type PurchaseDetailDTO struct {
|
|||||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type PurchaseItemDTO struct {
|
type PurchaseItemDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProductID uint `json:"product_id"`
|
ProductID uint `json:"product_id"`
|
||||||
@@ -64,16 +59,18 @@ type PurchaseItemDTO struct {
|
|||||||
TravelNumber *string `json:"travel_number"`
|
TravelNumber *string `json:"travel_number"`
|
||||||
TravelDocumentPath *string `json:"travel_document_path"`
|
TravelDocumentPath *string `json:"travel_document_path"`
|
||||||
VehicleNumber *string `json:"vehicle_number"`
|
VehicleNumber *string `json:"vehicle_number"`
|
||||||
|
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
|
||||||
|
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
||||||
return PurchaseRelationDTO{
|
return PurchaseRelationDTO{
|
||||||
Id: p.Id,
|
Id: p.Id,
|
||||||
PrNumber: p.PrNumber,
|
PrNumber: p.PrNumber,
|
||||||
PoNumber: p.PoNumber,
|
PoNumber: p.PoNumber,
|
||||||
PoDate: p.PoDate,
|
PoDate: p.PoDate,
|
||||||
Notes: p.Notes,
|
CreditTerm: p.CreditTerm,
|
||||||
|
Notes: p.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +109,20 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
|||||||
dto.Warehouse = &summary
|
dto.Warehouse = &summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.ExpenseNonstock != nil {
|
||||||
|
priceCopy := item.ExpenseNonstock.Price
|
||||||
|
dto.TransportPerItem = &priceCopy
|
||||||
|
|
||||||
|
if item.ExpenseNonstock.Expense != nil {
|
||||||
|
exp := item.ExpenseNonstock.Expense
|
||||||
|
|
||||||
|
if exp.Supplier != nil && exp.Supplier.Id != 0 {
|
||||||
|
supplierSummary := supplierDTO.ToSupplierRelationDTO(*exp.Supplier)
|
||||||
|
dto.ExpeditionVendor = &supplierSummary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,9 +156,7 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
|||||||
return PurchaseListDTO{
|
return PurchaseListDTO{
|
||||||
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
||||||
Supplier: supplier,
|
Supplier: supplier,
|
||||||
CreditTerm: p.CreditTerm,
|
|
||||||
DueDate: p.DueDate,
|
DueDate: p.DueDate,
|
||||||
GrandTotal: p.GrandTotal,
|
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: p.CreatedAt,
|
CreatedAt: p.CreatedAt,
|
||||||
UpdatedAt: p.UpdatedAt,
|
UpdatedAt: p.UpdatedAt,
|
||||||
@@ -188,9 +197,7 @@ func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
|||||||
return PurchaseDetailDTO{
|
return PurchaseDetailDTO{
|
||||||
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
||||||
Supplier: supplier,
|
Supplier: supplier,
|
||||||
CreditTerm: p.CreditTerm,
|
|
||||||
DueDate: p.DueDate,
|
DueDate: p.DueDate,
|
||||||
GrandTotal: p.GrandTotal,
|
|
||||||
Items: ToPurchaseItemDTOs(p.Items),
|
Items: ToPurchaseItemDTOs(p.Items),
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: p.CreatedAt,
|
CreatedAt: p.CreatedAt,
|
||||||
|
|||||||
@@ -8,15 +8,20 @@ import (
|
|||||||
|
|
||||||
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"
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
expenseService "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
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"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,13 +33,49 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||||
|
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||||
|
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||||
|
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
expenseBridge := service.NewNoopPurchaseExpenseBridge()
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
expenseServiceInstance := expenseService.NewExpenseService(
|
||||||
|
expenseRepository,
|
||||||
|
supplierRepo,
|
||||||
|
nonstockRepo,
|
||||||
|
approvalService,
|
||||||
|
expenseRealizationRepo,
|
||||||
|
projectFlockKandangRepository,
|
||||||
|
validate,
|
||||||
|
)
|
||||||
|
expenseBridge := service.NewExpenseBridge(
|
||||||
|
db,
|
||||||
|
purchaseRepo,
|
||||||
|
projectFlockKandangRepository,
|
||||||
|
expenseServiceInstance,
|
||||||
|
)
|
||||||
|
|
||||||
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
|
Key: fifo.StockableKey("PURCHASE_ITEMS"),
|
||||||
|
Table: "purchase_items",
|
||||||
|
Columns: fifo.StockableColumns{
|
||||||
|
ID: "id",
|
||||||
|
ProductWarehouseID: "product_warehouse_id",
|
||||||
|
TotalQuantity: "total_qty",
|
||||||
|
TotalUsedQuantity: "total_used",
|
||||||
|
CreatedAt: "id",
|
||||||
|
},
|
||||||
|
OrderBy: []string{"id ASC"},
|
||||||
|
})
|
||||||
|
|
||||||
purchaseService := service.NewPurchaseService(
|
purchaseService := service.NewPurchaseService(
|
||||||
validate,
|
validate,
|
||||||
@@ -43,8 +84,10 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
supplierRepo,
|
supplierRepo,
|
||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
|
projectFlockKandangRepository,
|
||||||
approvalService,
|
approvalService,
|
||||||
expenseBridge,
|
expenseBridge,
|
||||||
|
fifoService,
|
||||||
)
|
)
|
||||||
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ type PurchaseRepository interface {
|
|||||||
repository.BaseRepository[entity.Purchase]
|
repository.BaseRepository[entity.Purchase]
|
||||||
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||||
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
|
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
|
||||||
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate, grandTotal float64) error
|
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate) error
|
||||||
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
|
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
|
||||||
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
||||||
UpdateGrandTotal(ctx context.Context, purchaseID uint, grandTotal float64) error
|
|
||||||
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
|
BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseRepositoryImpl struct {
|
type PurchaseRepositoryImpl struct {
|
||||||
@@ -59,6 +59,34 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error {
|
||||||
|
if purchaseID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
WITH latest_pfk AS (
|
||||||
|
SELECT pfk.id, pfk.kandang_id
|
||||||
|
FROM project_flock_kandangs pfk
|
||||||
|
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 = pfk.project_flock_id
|
||||||
|
WHERE LOWER(latest_approval.step_name) = LOWER('Aktif')
|
||||||
|
)
|
||||||
|
UPDATE purchase_items pi
|
||||||
|
SET project_flock_kandang_id = lp.id
|
||||||
|
FROM warehouses w
|
||||||
|
JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id
|
||||||
|
WHERE pi.purchase_id = ?
|
||||||
|
AND pi.project_flock_kandang_id IS NULL
|
||||||
|
AND pi.warehouse_id = w.id;
|
||||||
|
`
|
||||||
|
return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -99,7 +127,6 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
purchaseID uint,
|
purchaseID uint,
|
||||||
updates []PurchasePricingUpdate,
|
updates []PurchasePricingUpdate,
|
||||||
grandTotal float64,
|
|
||||||
) error {
|
) error {
|
||||||
if len(updates) == 0 {
|
if len(updates) == 0 {
|
||||||
return errors.New("pricing updates cannot be empty")
|
return errors.New("pricing updates cannot be empty")
|
||||||
@@ -133,14 +160,6 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Model(&entity.Purchase{}).
|
|
||||||
Where("id = ?", purchaseID).
|
|
||||||
Updates(map[string]interface{}{
|
|
||||||
"grand_total": grandTotal,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,20 +220,6 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
|
|
||||||
ctx context.Context,
|
|
||||||
purchaseID uint,
|
|
||||||
grandTotal float64,
|
|
||||||
) error {
|
|
||||||
return r.DB().WithContext(ctx).
|
|
||||||
Model(&entity.Purchase{}).
|
|
||||||
Where("id = ?", purchaseID).
|
|
||||||
Updates(map[string]interface{}{
|
|
||||||
"grand_total": grandTotal,
|
|
||||||
"updated_at": gorm.Expr("NOW()"),
|
|
||||||
}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
|
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
|
||||||
if len(itemIDs) == 0 {
|
if len(itemIDs) == 0 {
|
||||||
return errors.New("itemIDs cannot be empty")
|
return errors.New("itemIDs cannot be empty")
|
||||||
|
|||||||
@@ -2,42 +2,654 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
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"
|
||||||
|
expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
|
|
||||||
type PurchaseExpenseBridge interface {
|
type PurchaseExpenseBridge interface {
|
||||||
OnItemsCreated(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
|
OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
|
||||||
OnItemsDeleted(ctx context.Context, purchaseID uint, itemIDs []uint) error
|
OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error
|
||||||
OnItemsReceived(ctx context.Context, purchaseID uint, updates []ExpenseReceivingPayload) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
|
|
||||||
type ExpenseReceivingPayload struct {
|
type ExpenseReceivingPayload struct {
|
||||||
PurchaseItemID uint
|
PurchaseItemID uint
|
||||||
ProductID uint
|
ProductID uint
|
||||||
WarehouseID uint
|
WarehouseID uint
|
||||||
ReceivedQty float64
|
SupplierID uint
|
||||||
ReceivedDate *time.Time
|
TransportPerItem *float64
|
||||||
|
ReceivedQty float64
|
||||||
|
ReceivedDate *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// noopPurchaseExpenseBridge is the default implementation until the expense module is ready.
|
type groupedItem struct {
|
||||||
type noopPurchaseExpenseBridge struct{}
|
item *entity.PurchaseItem
|
||||||
|
payload ExpenseReceivingPayload
|
||||||
func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
|
projectFK *uint
|
||||||
return &noopPurchaseExpenseBridge{}
|
kandangID *uint
|
||||||
|
totalPrice float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint, _ []entity.PurchaseItem) error {
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
||||||
|
return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type expenseBridge struct {
|
||||||
|
db *gorm.DB
|
||||||
|
purchaseRepo rPurchase.PurchaseRepository
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
|
expenseSvc expenseSvc.ExpenseService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseBridge(
|
||||||
|
db *gorm.DB,
|
||||||
|
purchaseRepo rPurchase.PurchaseRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||||
|
expenseSvc expenseSvc.ExpenseService,
|
||||||
|
) PurchaseExpenseBridge {
|
||||||
|
return &expenseBridge{
|
||||||
|
db: db,
|
||||||
|
purchaseRepo: purchaseRepo,
|
||||||
|
projectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
expenseSvc: expenseSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []entity.PurchaseItem) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
expenseIDs := make(map[uint64]struct{})
|
||||||
|
expenseNonstockIDs := make([]uint64, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
||||||
|
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(expenseNonstockIDs) > 0 {
|
||||||
|
for _, nsID := range expenseNonstockIDs {
|
||||||
|
var expenseID uint64
|
||||||
|
if err := tx.
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Select("expense_id").
|
||||||
|
Where("id = ?", nsID).
|
||||||
|
Scan(&expenseID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if expenseID != 0 {
|
||||||
|
expenseIDs[expenseID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var links []struct {
|
||||||
|
ItemID uint
|
||||||
|
ExpenseNonstockID *uint64
|
||||||
|
}
|
||||||
|
if err := tx.
|
||||||
|
Model(&entity.PurchaseItem{}).
|
||||||
|
Select("id as item_id, expense_nonstock_id").
|
||||||
|
Where("id IN ?", extractIDs(items)).
|
||||||
|
Scan(&links).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, link := range links {
|
||||||
|
if link.ExpenseNonstockID == nil || *link.ExpenseNonstockID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var expenseID uint64
|
||||||
|
if err := tx.
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Select("expense_id").
|
||||||
|
Where("id = ?", *link.ExpenseNonstockID).
|
||||||
|
Scan(&expenseID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if expenseID != 0 {
|
||||||
|
expenseIDs[expenseID] = struct{}{}
|
||||||
|
}
|
||||||
|
if err := tx.Delete(&entity.ExpenseNonstock{}, *link.ExpenseNonstockID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||||
|
for expenseID := range expenseIDs {
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("expense_id = ?", expenseID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
||||||
|
if len(expenseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if actorID == 0 {
|
||||||
|
actorID = 1
|
||||||
|
}
|
||||||
|
svc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
|
action := entity.ApprovalActionUpdated
|
||||||
|
for id := range expenseIDs {
|
||||||
|
if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint, _ []uint) error {
|
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
|
||||||
|
if purchaseID == 0 || len(updates) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Context()
|
||||||
|
|
||||||
|
// Load current links to decide whether to update in place or recreate.
|
||||||
|
type itemLink struct {
|
||||||
|
ExpenseNonstockID uint64
|
||||||
|
ExpenseID uint64
|
||||||
|
SupplierID uint
|
||||||
|
TransactionDate time.Time
|
||||||
|
Qty float64
|
||||||
|
Price float64
|
||||||
|
}
|
||||||
|
|
||||||
|
purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("Items").
|
||||||
|
Preload("Items.Warehouse").
|
||||||
|
Preload("Items.Warehouse.Kandang")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
itemLinks := make(map[uint]itemLink)
|
||||||
|
updatedExpenses := make(map[uint64]struct{})
|
||||||
|
if len(updates) > 0 {
|
||||||
|
ids := make([]uint, 0, len(updates))
|
||||||
|
for _, upd := range updates {
|
||||||
|
if upd.PurchaseItemID != 0 {
|
||||||
|
ids = append(ids, upd.PurchaseItemID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
rows := make([]struct {
|
||||||
|
ItemID uint
|
||||||
|
ExpenseNonstockID uint64
|
||||||
|
ExpenseID uint64
|
||||||
|
SupplierID uint
|
||||||
|
TransactionDate time.Time
|
||||||
|
Qty float64
|
||||||
|
Price float64
|
||||||
|
}, 0)
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Table("purchase_items pi").
|
||||||
|
Select("pi.id as item_id, en.id as expense_nonstock_id, en.expense_id, e.supplier_id, e.transaction_date, en.qty, en.price").
|
||||||
|
Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
|
||||||
|
Joins("LEFT JOIN expenses e ON e.id = en.expense_id").
|
||||||
|
Where("pi.id IN ?", ids).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Build quick lookup per item and per group key for existing expenses.
|
||||||
|
for _, row := range rows {
|
||||||
|
itemLinks[row.ItemID] = itemLink{
|
||||||
|
ExpenseNonstockID: row.ExpenseNonstockID,
|
||||||
|
ExpenseID: row.ExpenseID,
|
||||||
|
SupplierID: row.SupplierID,
|
||||||
|
TransactionDate: row.TransactionDate,
|
||||||
|
Qty: row.Qty,
|
||||||
|
Price: row.Price,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
||||||
|
for i := range purchase.Items {
|
||||||
|
itemMap[purchase.Items[i].Id] = &purchase.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := make(map[string][]groupedItem)
|
||||||
|
|
||||||
|
for _, payload := range updates {
|
||||||
|
if payload.ReceivedDate == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
|
||||||
|
}
|
||||||
|
item := itemMap[payload.PurchaseItemID]
|
||||||
|
if item == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||||
|
}
|
||||||
|
if payload.ReceivedQty <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d must be greater than 0", payload.PurchaseItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
|
||||||
|
supplierID := payload.SupplierID
|
||||||
|
if supplierID == 0 {
|
||||||
|
supplierID = purchase.SupplierId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide whether to update existing expense_nonstock or create new.
|
||||||
|
link, hasLink := itemLinks[payload.PurchaseItemID]
|
||||||
|
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||||
|
oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
|
||||||
|
newDate := receivedDate
|
||||||
|
oldSupplier := link.SupplierID
|
||||||
|
pricePerItem := item.Price
|
||||||
|
if payload.TransportPerItem != nil {
|
||||||
|
pricePerItem = *payload.TransportPerItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// If supplier/date unchanged, update nonstock in place.
|
||||||
|
if oldSupplier == supplierID && oldDate.Equal(newDate) {
|
||||||
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("id = ?", link.ExpenseNonstockID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"qty": payload.ReceivedQty,
|
||||||
|
"price": pricePerItem,
|
||||||
|
"notes": note,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if link.ExpenseID != 0 {
|
||||||
|
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supplier/date changed: if the linked expense has only this nonstock, update it in place.
|
||||||
|
if link.ExpenseID != 0 {
|
||||||
|
var cnt int64
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("expense_id = ?", link.ExpenseID).
|
||||||
|
Count(&cnt).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cnt == 1 {
|
||||||
|
if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
||||||
|
}
|
||||||
|
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.Expense{}).
|
||||||
|
Where("id = ?", link.ExpenseID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"transaction_date": newDate,
|
||||||
|
"supplier_id": supplierID,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateBody := map[string]interface{}{
|
||||||
|
"qty": payload.ReceivedQty,
|
||||||
|
"price": pricePerItem,
|
||||||
|
"notes": note,
|
||||||
|
"nonstock_id": newNonstockID,
|
||||||
|
"kandang_id": uint64(*item.Warehouse.KandangId),
|
||||||
|
}
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("id = ?", link.ExpenseNonstockID).
|
||||||
|
Updates(updateBody).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expense has multiple nonstocks: create new expense header for this item, then move existing nonstock to it.
|
||||||
|
var kandangID *uint
|
||||||
|
var projectFK *uint
|
||||||
|
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||||
|
id := uint(*item.Warehouse.KandangId)
|
||||||
|
kandangID = &id
|
||||||
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||||
|
pid := uint(project.Id)
|
||||||
|
projectFK = &pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pricePerItem := item.Price
|
||||||
|
if payload.TransportPerItem != nil {
|
||||||
|
pricePerItem = *payload.TransportPerItem
|
||||||
|
}
|
||||||
|
totalPrice := pricePerItem * payload.ReceivedQty
|
||||||
|
|
||||||
|
gItem := groupedItem{
|
||||||
|
item: item,
|
||||||
|
payload: payload,
|
||||||
|
projectFK: projectFK,
|
||||||
|
kandangID: kandangID,
|
||||||
|
totalPrice: totalPrice,
|
||||||
|
}
|
||||||
|
|
||||||
|
newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseDetail, err := b.createExpenseViaService(c, purchase, []groupedItem{gItem}, newDate, newNonstockID, purchase.PoNumber, supplierID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdNonstockID uint64
|
||||||
|
if expenseDetail != nil {
|
||||||
|
noteMap := mapExpenseNotes(expenseDetail)
|
||||||
|
createdNonstockID = noteMap[payload.PurchaseItemID]
|
||||||
|
}
|
||||||
|
|
||||||
|
note := fmt.Sprintf("purchase_item:%d", payload.PurchaseItemID)
|
||||||
|
updateBody := map[string]interface{}{
|
||||||
|
"expense_id": expenseDetail.Id,
|
||||||
|
"qty": payload.ReceivedQty,
|
||||||
|
"price": pricePerItem,
|
||||||
|
"notes": note,
|
||||||
|
"nonstock_id": newNonstockID,
|
||||||
|
}
|
||||||
|
if kandangID != nil {
|
||||||
|
updateBody["kandang_id"] = uint64(*kandangID)
|
||||||
|
}
|
||||||
|
if projectFK != nil {
|
||||||
|
updateBody["project_flock_kandang_id"] = uint64(*projectFK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.ExpenseNonstock{}).
|
||||||
|
Where("id = ?", link.ExpenseNonstockID).
|
||||||
|
Updates(updateBody).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if createdNonstockID != 0 {
|
||||||
|
if err := b.db.WithContext(ctx).Delete(&entity.ExpenseNonstock{}, createdNonstockID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if link.ExpenseID != 0 {
|
||||||
|
updatedExpenses[link.ExpenseID] = struct{}{}
|
||||||
|
}
|
||||||
|
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||||
|
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise create new expense/nonstock in grouping flow.
|
||||||
|
}
|
||||||
|
|
||||||
|
baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
|
||||||
|
key := baseKey
|
||||||
|
if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
|
||||||
|
key = fmt.Sprintf("%s:%d", baseKey, payload.PurchaseItemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandangID *uint
|
||||||
|
var projectFK *uint
|
||||||
|
if item.Warehouse != nil && item.Warehouse.KandangId != nil {
|
||||||
|
id := uint(*item.Warehouse.KandangId)
|
||||||
|
kandangID = &id
|
||||||
|
if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
|
||||||
|
pid := uint(project.Id)
|
||||||
|
projectFK = &pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pricePerItem := item.Price
|
||||||
|
if payload.TransportPerItem != nil {
|
||||||
|
pricePerItem = *payload.TransportPerItem
|
||||||
|
}
|
||||||
|
totalPrice := pricePerItem * payload.ReceivedQty
|
||||||
|
|
||||||
|
groups[key] = append(groups[key], groupedItem{
|
||||||
|
item: item,
|
||||||
|
payload: payload,
|
||||||
|
projectFK: projectFK,
|
||||||
|
kandangID: kandangID,
|
||||||
|
totalPrice: totalPrice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, items := range groups {
|
||||||
|
if len(items) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Split(key, ":")
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return errors.New("invalid expense grouping key")
|
||||||
|
}
|
||||||
|
expenseDate, err := utils.ParseDateString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
supplierID, err := strconv.ParseUint(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseDetail, err := b.createExpenseViaService(c, purchase, items, expenseDate, expeditionNonstockID, purchase.PoNumber, uint(supplierID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if expenseDetail != nil && expenseDetail.Id != 0 {
|
||||||
|
updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updatedExpenses) > 0 {
|
||||||
|
if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint, _ []ExpenseReceivingPayload) error {
|
func (b *expenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
|
||||||
|
var id uint64
|
||||||
|
err := b.db.WithContext(ctx).
|
||||||
|
Table("nonstocks AS ns").
|
||||||
|
Select("ns.id").
|
||||||
|
Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
|
||||||
|
Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
|
||||||
|
Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
|
||||||
|
Where("nss.supplier_id = ?", supplierID).
|
||||||
|
Order("ns.id").
|
||||||
|
Limit(1).
|
||||||
|
Scan(&id).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if id == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractIDs(items []entity.PurchaseItem) []uint {
|
||||||
|
result := make([]uint, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if item.Id != 0 {
|
||||||
|
result = append(result, item.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *expenseBridge) createExpenseViaService(
|
||||||
|
c *fiber.Ctx,
|
||||||
|
purchase *entity.Purchase,
|
||||||
|
items []groupedItem,
|
||||||
|
expenseDate time.Time,
|
||||||
|
expeditionNonstockID uint64,
|
||||||
|
poNumber *string,
|
||||||
|
supplierID uint,
|
||||||
|
) (*expenseDto.ExpenseDetailDTO, error) {
|
||||||
|
ctx := c.Context()
|
||||||
|
if b.expenseSvc == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := items[0].kandangID
|
||||||
|
if kandangID == nil || *kandangID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs")
|
||||||
|
}
|
||||||
|
|
||||||
|
costItems := make([]expenseValidation.CostItem, 0, len(items))
|
||||||
|
for _, gi := range items {
|
||||||
|
note := fmt.Sprintf("purchase_item:%d", gi.payload.PurchaseItemID)
|
||||||
|
price := gi.item.Price
|
||||||
|
if gi.payload.TransportPerItem != nil {
|
||||||
|
price = *gi.payload.TransportPerItem
|
||||||
|
}
|
||||||
|
costItems = append(costItems, expenseValidation.CostItem{
|
||||||
|
NonstockID: expeditionNonstockID,
|
||||||
|
Quantity: gi.payload.ReceivedQty,
|
||||||
|
Price: price,
|
||||||
|
Notes: note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &expenseValidation.Create{
|
||||||
|
PoNumber: "",
|
||||||
|
TransactionDate: utils.FormatDate(expenseDate),
|
||||||
|
Category: "BOP",
|
||||||
|
SupplierID: uint64(supplierID),
|
||||||
|
ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
|
||||||
|
KandangID: uint64(*kandangID),
|
||||||
|
CostItems: costItems,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
if poNumber != nil {
|
||||||
|
req.PoNumber = *poNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
detail, err := b.expenseSvc.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark approvals up to Finance so latest is Manager Finance
|
||||||
|
action := entity.ApprovalActionApproved
|
||||||
|
actorID := uint(purchase.CreatedBy)
|
||||||
|
if actorID == 0 {
|
||||||
|
actorID = 1
|
||||||
|
}
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedItem) error {
|
||||||
|
if detail == nil || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
noteToExpenseNonstock := mapExpenseNotes(detail)
|
||||||
|
|
||||||
|
if len(noteToExpenseNonstock) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gi := range items {
|
||||||
|
expenseNonstockID, ok := noteToExpenseNonstock[gi.payload.PurchaseItemID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.db.WithContext(ctx).
|
||||||
|
Model(&entity.PurchaseItem{}).
|
||||||
|
Where("id = ?", gi.payload.PurchaseItemID).
|
||||||
|
Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
|
||||||
|
result := make(map[uint]uint64)
|
||||||
|
if detail == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, kandang := range detail.Kandangs {
|
||||||
|
for _, pengajuan := range kandang.Pengajuans {
|
||||||
|
note := strings.TrimSpace(pengajuan.Notes)
|
||||||
|
if note == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prefix = "purchase_item:"
|
||||||
|
if !strings.HasPrefix(note, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idStr := strings.TrimPrefix(note, prefix)
|
||||||
|
var itemID uint
|
||||||
|
if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[itemID] = pengajuan.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@ type PurchaseItemPayload struct {
|
|||||||
|
|
||||||
type CreatePurchaseRequest struct {
|
type CreatePurchaseRequest struct {
|
||||||
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||||
CreditTerm int `json:"credit_term" validate:"required,gte=0"`
|
CreditTerm int `json:"credit_term" validate:"required,number,gte=0"`
|
||||||
|
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
@@ -38,6 +39,8 @@ type ReceivePurchaseItemRequest struct {
|
|||||||
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"`
|
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"`
|
||||||
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
|
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
|
||||||
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
|
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
|
TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
|
||||||
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
|
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
|
||||||
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
|
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
|
||||||
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
|
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepportController struct {
|
||||||
|
RepportService service.RepportService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepportController(repportService service.RepportService) *RepportController {
|
||||||
|
return &RepportController{
|
||||||
|
RepportService: repportService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RepportController) GetAll(ctx *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: ctx.QueryInt("page", 1),
|
||||||
|
Limit: ctx.QueryInt("limit", 10),
|
||||||
|
Search: ctx.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := c.RepportService.GetAll(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.RepportListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all reports successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RepportController) GetOne(ctx *fiber.Ctx) error {
|
||||||
|
param := ctx.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.RepportService.GetOne(ctx, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get report successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
|
||||||
|
param := ctx.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.RepportService.GetOne(ctx, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get report successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type RepportListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepportDetailDTO struct {
|
||||||
|
RepportListDTO
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package repports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
|
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepportModule struct{}
|
||||||
|
|
||||||
|
func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
// Initialize expense realization repository
|
||||||
|
expRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||||
|
|
||||||
|
// Initialize report service with expense realization repo
|
||||||
|
repportService := sRepport.NewRepportService(validate, expRealizationRepo)
|
||||||
|
|
||||||
|
RepportRoutes(router, repportService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package repports
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers"
|
||||||
|
repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
|
||||||
|
ctrl := controller.NewRepportController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/repports")
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
|
||||||
|
route.Get("expense", ctrl.GetExpense)
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepportService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
|
||||||
|
GetExpense(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repportService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
dummyData map[uint]dto.RepportListDTO
|
||||||
|
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository) RepportService {
|
||||||
|
// Initialize with dummy data
|
||||||
|
now := time.Now()
|
||||||
|
dummyData := map[uint]dto.RepportListDTO{
|
||||||
|
1: {
|
||||||
|
Id: 1,
|
||||||
|
Name: "Sales Report",
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
Id: 2,
|
||||||
|
Name: "Inventory Report",
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
Id: 3,
|
||||||
|
Name: "Production Report",
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &repportService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
dummyData: dummyData,
|
||||||
|
ExpenseRealizationRepo: expenseRealizationRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *repportService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to slice
|
||||||
|
var results []dto.RepportListDTO
|
||||||
|
for _, v := range s.dummyData {
|
||||||
|
// Apply search filter if provided
|
||||||
|
if params.Search != "" && !strings.Contains(strings.ToLower(v.Name), strings.ToLower(params.Search)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
total := int64(len(results))
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
if offset >= int(total) {
|
||||||
|
return []dto.RepportListDTO{}, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end := offset + params.Limit
|
||||||
|
if end > int(total) {
|
||||||
|
end = int(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results[offset:end], total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *repportService) GetOne(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
|
||||||
|
if data, ok := s.dummyData[id]; ok {
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *repportService) GetExpense(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
|
||||||
|
if data, ok := s.dummyData[id]; ok {
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user