mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat: export input progress report for expenses, marketings, purchases, and recordings
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
@@ -27,6 +30,25 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
|
||||
}
|
||||
|
||||
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||
if exportprogress.IsProgressExportRequest(c) {
|
||||
query, err := exportprogress.ParseQuery(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := u.DeliveryOrdersService.GetProgressRows(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := exportprogress.BuildWorkbook("Marketings", query, rows)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate progress excel file")
|
||||
}
|
||||
filename := fmt.Sprintf("marketings_progress_%s.xlsx", time.Now().Format("20060102_150405"))
|
||||
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
return c.Status(fiber.StatusOK).Send(content)
|
||||
}
|
||||
|
||||
parseUintListParam := func(param string) ([]uint, error) {
|
||||
if param == "" {
|
||||
return nil, nil
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestMarketingRepositoryGetProgressRows(t *testing.T) {
|
||||
db := openMarketingProgressTestDB(t)
|
||||
repo := NewMarketingRepository(db)
|
||||
|
||||
mustExecMarketing(t, db, `CREATE TABLE locations (id INTEGER PRIMARY KEY, name TEXT)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE project_flocks (id INTEGER PRIMARY KEY, flock_name TEXT)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE kandangs (id INTEGER PRIMARY KEY, name TEXT, location_id INTEGER)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE project_flock_kandangs (id INTEGER PRIMARY KEY, project_flock_id INTEGER, kandang_id INTEGER)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE warehouses (id INTEGER PRIMARY KEY, location_id INTEGER, kandang_id INTEGER)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE product_warehouses (id INTEGER PRIMARY KEY, warehouse_id INTEGER, project_flock_kandang_id INTEGER)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE marketings (id INTEGER PRIMARY KEY, so_date DATE, deleted_at DATETIME)`)
|
||||
mustExecMarketing(t, db, `CREATE TABLE marketing_products (id INTEGER PRIMARY KEY, marketing_id INTEGER, product_warehouse_id INTEGER)`)
|
||||
|
||||
mustExecMarketing(t, db, `INSERT INTO locations (id, name) VALUES (1, 'Location A'), (2, 'Location B')`)
|
||||
mustExecMarketing(t, db, `INSERT INTO project_flocks (id, flock_name) VALUES (1, 'Farm A')`)
|
||||
mustExecMarketing(t, db, `INSERT INTO kandangs (id, name, location_id) VALUES (1, 'Kandang 1', 1)`)
|
||||
mustExecMarketing(t, db, `INSERT INTO project_flock_kandangs (id, project_flock_id, kandang_id) VALUES (1, 1, 1)`)
|
||||
mustExecMarketing(t, db, `INSERT INTO warehouses (id, location_id, kandang_id) VALUES (1, 1, 1), (2, 2, NULL)`)
|
||||
mustExecMarketing(t, db, `INSERT INTO product_warehouses (id, warehouse_id, project_flock_kandang_id) VALUES (1, 1, 1), (2, 2, NULL)`)
|
||||
mustExecMarketing(t, db, `INSERT INTO marketings (id, so_date, deleted_at) VALUES (1, '2026-06-12', NULL), (2, '2026-06-12', NULL)`)
|
||||
mustExecMarketing(t, db, `INSERT INTO marketing_products (id, marketing_id, product_warehouse_id) VALUES
|
||||
(1, 1, 1),
|
||||
(2, 1, 1),
|
||||
(3, 2, 2)`)
|
||||
|
||||
rows, err := repo.GetProgressRows(context.Background(), time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC), nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("GetProgressRows failed: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 grouped rows, got %d", len(rows))
|
||||
}
|
||||
assertProgressRowMarketing(t, rows, "Farm A", "Kandang 1", "2026-06-12", 1)
|
||||
assertProgressRowMarketing(t, rows, "Location B", "Farm-level / Unassigned", "2026-06-12", 1)
|
||||
}
|
||||
|
||||
func openMarketingProgressTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func mustExecMarketing(t *testing.T, db *gorm.DB, query string, args ...any) {
|
||||
t.Helper()
|
||||
if err := db.Exec(query, args...).Error; err != nil {
|
||||
t.Fatalf("exec failed for %q: %v", query, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertProgressRowMarketing(t *testing.T, rows []exportprogress.Row, farm, kandang, date string, count int) {
|
||||
t.Helper()
|
||||
for _, row := range rows {
|
||||
if row.FarmName == farm && row.KandangName == kandang && row.ActivityDate.Format("2006-01-02") == date && row.Count == count {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected row farm=%s kandang=%s date=%s count=%d, got %+v", farm, kandang, date, count, rows)
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,6 +16,7 @@ type MarketingRepository interface {
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetNextSequence(ctx context.Context) (uint, error)
|
||||
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error)
|
||||
}
|
||||
|
||||
type MarketingRepositoryImpl struct {
|
||||
@@ -55,3 +58,67 @@ func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB)
|
||||
|
||||
return soNumber, nil
|
||||
}
|
||||
|
||||
func (r *MarketingRepositoryImpl) GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) {
|
||||
const unassignedSQL = "'" + exportprogress.UnassignedKandangName + "'"
|
||||
subQuery := r.DB().WithContext(ctx).
|
||||
Table("marketings AS m").
|
||||
Select(`
|
||||
DISTINCT m.id AS marketing_id,
|
||||
'Marketings' AS module,
|
||||
COALESCE(pf.flock_name, loc.name, 'Unknown Farm') AS farm_name,
|
||||
COALESCE(k.name, `+unassignedSQL+`, 'Unknown Kandang') AS kandang_name,
|
||||
DATE(m.so_date) AS activity_date
|
||||
`).
|
||||
Joins("JOIN marketing_products mp ON mp.marketing_id = m.id").
|
||||
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = pw.project_flock_kandang_id").
|
||||
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("LEFT JOIN kandangs k ON k.id = COALESCE(pfk.kandang_id, w.kandang_id)").
|
||||
Joins("LEFT JOIN locations loc ON loc.id = COALESCE(k.location_id, w.location_id)").
|
||||
Where("m.deleted_at IS NULL").
|
||||
Where("DATE(m.so_date) >= DATE(?)", startDate).
|
||||
Where("DATE(m.so_date) <= DATE(?)", endDate)
|
||||
|
||||
if restrict {
|
||||
if len(allowedLocationIDs) == 0 {
|
||||
return []exportprogress.Row{}, nil
|
||||
}
|
||||
subQuery = subQuery.Where("w.location_id IN ?", allowedLocationIDs)
|
||||
}
|
||||
|
||||
type progressRowResult struct {
|
||||
Module string
|
||||
FarmName string
|
||||
KandangName string
|
||||
ActivityDate string
|
||||
Count int
|
||||
}
|
||||
scanned := make([]progressRowResult, 0)
|
||||
err := r.DB().WithContext(ctx).
|
||||
Table("(?) AS progress_rows", subQuery).
|
||||
Select("module, farm_name, kandang_name, activity_date, COUNT(*) AS count").
|
||||
Group("module, farm_name, kandang_name, activity_date").
|
||||
Order("activity_date ASC, farm_name ASC, kandang_name ASC").
|
||||
Scan(&scanned).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows := make([]exportprogress.Row, 0, len(scanned))
|
||||
for _, item := range scanned {
|
||||
activityDate, err := time.Parse("2006-01-02", item.ActivityDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows = append(rows, exportprogress.Row{
|
||||
Module: item.Module,
|
||||
FarmName: item.FarmName,
|
||||
KandangName: item.KandangName,
|
||||
ActivityDate: activityDate,
|
||||
Count: item.Count,
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2"
|
||||
@@ -34,6 +35,7 @@ type DeliveryOrdersService interface {
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
||||
BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error)
|
||||
GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
|
||||
}
|
||||
|
||||
type deliveryOrdersService struct {
|
||||
@@ -249,6 +251,14 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) GetProgressRows(c *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error) {
|
||||
scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.MarketingRepo.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict)
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) {
|
||||
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user