Update purchase analytics
This commit is contained in:
parent
c3db919531
commit
d0c090a657
@ -106,7 +106,11 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
|
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -116,7 +120,11 @@ type PurchasingSummary struct {
|
|||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -38,7 +38,11 @@ type PurchasingAnalytics struct {
|
|||||||
|
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -48,7 +52,11 @@ type PurchasingSummary struct {
|
|||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -113,7 +113,11 @@ type PurchasingAnalyticsResponse struct {
|
|||||||
// PurchasingSummary represents the summary of purchasing analytics
|
// PurchasingSummary represents the summary of purchasing analytics
|
||||||
type PurchasingSummary struct {
|
type PurchasingSummary struct {
|
||||||
TotalPurchases float64 `json:"total_purchases"`
|
TotalPurchases float64 `json:"total_purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
TotalPurchaseOrders int64 `json:"total_purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
TotalQuantity float64 `json:"total_quantity"`
|
TotalQuantity float64 `json:"total_quantity"`
|
||||||
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
AveragePurchaseOrderValue float64 `json:"average_purchase_order_value"`
|
||||||
TotalIngredients int64 `json:"total_ingredients"`
|
TotalIngredients int64 `json:"total_ingredients"`
|
||||||
@ -124,7 +128,11 @@ type PurchasingSummary struct {
|
|||||||
type PurchasingAnalyticsData struct {
|
type PurchasingAnalyticsData struct {
|
||||||
Date time.Time `json:"date"`
|
Date time.Time `json:"date"`
|
||||||
Purchases float64 `json:"purchases"`
|
Purchases float64 `json:"purchases"`
|
||||||
|
RawMaterialPurchases float64 `json:"raw_material_purchases"`
|
||||||
|
NonInventoryPurchases float64 `json:"non_inventory_purchases"`
|
||||||
PurchaseOrders int64 `json:"purchase_orders"`
|
PurchaseOrders int64 `json:"purchase_orders"`
|
||||||
|
RawMaterialPurchaseOrders int64 `json:"raw_material_purchase_orders"`
|
||||||
|
NonInventoryExpenseCount int64 `json:"non_inventory_expense_count"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Ingredients int64 `json:"ingredients"`
|
Ingredients int64 `json:"ingredients"`
|
||||||
Vendors int64 `json:"vendors"`
|
Vendors int64 `json:"vendors"`
|
||||||
|
|||||||
@ -187,7 +187,11 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
data[i] = models.PurchasingAnalyticsData{
|
data[i] = models.PurchasingAnalyticsData{
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
|
NonInventoryPurchases: item.NonInventoryPurchases,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
|
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Ingredients: item.Ingredients,
|
Ingredients: item.Ingredients,
|
||||||
Vendors: item.Vendors,
|
Vendors: item.Vendors,
|
||||||
@ -227,7 +231,11 @@ func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req
|
|||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
Summary: models.PurchasingSummary{
|
Summary: models.PurchasingSummary{
|
||||||
TotalPurchases: result.Summary.TotalPurchases,
|
TotalPurchases: result.Summary.TotalPurchases,
|
||||||
|
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
||||||
|
NonInventoryPurchases: result.Summary.NonInventoryPurchases,
|
||||||
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
||||||
|
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
||||||
|
NonInventoryExpenseCount: result.Summary.NonInventoryExpenseCount,
|
||||||
TotalQuantity: result.Summary.TotalQuantity,
|
TotalQuantity: result.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: result.Summary.TotalIngredients,
|
TotalIngredients: result.Summary.TotalIngredients,
|
||||||
|
|||||||
@ -75,7 +75,23 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
purchasingResult: &entities.PurchasingAnalytics{
|
purchasingResult: &entities.PurchasingAnalytics{
|
||||||
OutletName: &outletName,
|
OutletName: &outletName,
|
||||||
Summary: entities.PurchasingSummary{
|
Summary: entities.PurchasingSummary{
|
||||||
TotalPurchases: 125,
|
TotalPurchases: 300,
|
||||||
|
RawMaterialPurchases: 125,
|
||||||
|
NonInventoryPurchases: 175,
|
||||||
|
TotalPurchaseOrders: 3,
|
||||||
|
RawMaterialPurchaseOrders: 1,
|
||||||
|
NonInventoryExpenseCount: 2,
|
||||||
|
},
|
||||||
|
Data: []entities.PurchasingAnalyticsData{
|
||||||
|
{
|
||||||
|
Date: now,
|
||||||
|
Purchases: 300,
|
||||||
|
RawMaterialPurchases: 125,
|
||||||
|
NonInventoryPurchases: 175,
|
||||||
|
PurchaseOrders: 3,
|
||||||
|
RawMaterialPurchaseOrders: 1,
|
||||||
|
NonInventoryExpenseCount: 2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, expenseRepositoryStub{})
|
}, expenseRepositoryStub{})
|
||||||
@ -92,7 +108,16 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
|
|||||||
require.Equal(t, &outletID, result.OutletID)
|
require.Equal(t, &outletID, result.OutletID)
|
||||||
require.NotNil(t, result.OutletName)
|
require.NotNil(t, result.OutletName)
|
||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
require.Equal(t, float64(125), result.Summary.TotalPurchases)
|
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
||||||
|
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
||||||
|
require.Equal(t, float64(175), result.Summary.NonInventoryPurchases)
|
||||||
|
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
||||||
|
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
||||||
|
require.Equal(t, int64(2), result.Summary.NonInventoryExpenseCount)
|
||||||
|
require.Len(t, result.Data, 1)
|
||||||
|
require.Equal(t, float64(300), result.Data[0].Purchases)
|
||||||
|
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
||||||
|
require.Equal(t, float64(175), result.Data[0].NonInventoryPurchases)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *testing.T) {
|
||||||
|
|||||||
@ -145,68 +145,179 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryQuery := r.db.WithContext(ctx).
|
rawMaterialOutletFilter := ""
|
||||||
Table("inventory_movements im").
|
nonInventoryOutletFilter := ""
|
||||||
Select(`
|
rawMaterialSummaryArgs := []interface{}{
|
||||||
COALESCE(SUM(im.total_cost), 0) as total_purchases,
|
organizationID,
|
||||||
COUNT(DISTINCT im.reference_id) as total_purchase_orders,
|
entities.InventoryMovementTypePurchase,
|
||||||
|
"INGREDIENT",
|
||||||
|
entities.InventoryMovementReferenceTypePurchaseOrder,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
nonInventorySummaryArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.PurchaseCategoryTypeNonInventory,
|
||||||
|
"approved",
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
if outletID != nil {
|
||||||
|
rawMaterialOutletFilter = "AND im.outlet_id = ?"
|
||||||
|
nonInventoryOutletFilter = "AND e.outlet_id = ?"
|
||||||
|
rawMaterialSummaryArgs = append(rawMaterialSummaryArgs, *outletID)
|
||||||
|
nonInventorySummaryArgs = append(nonInventorySummaryArgs, *outletID)
|
||||||
|
}
|
||||||
|
summaryArgs := append(rawMaterialSummaryArgs, nonInventorySummaryArgs...)
|
||||||
|
|
||||||
|
summaryQuery := `
|
||||||
|
WITH raw_material AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(im.total_cost), 0) as raw_material_purchases,
|
||||||
|
COUNT(DISTINCT im.reference_id) as raw_material_purchase_orders,
|
||||||
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
COALESCE(SUM(im.quantity), 0) as total_quantity,
|
||||||
CASE
|
|
||||||
WHEN COUNT(DISTINCT im.reference_id) > 0
|
|
||||||
THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id)
|
|
||||||
ELSE 0
|
|
||||||
END as average_purchase_order_value,
|
|
||||||
COUNT(DISTINCT im.item_id) as total_ingredients,
|
COUNT(DISTINCT im.item_id) as total_ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as total_vendors
|
COUNT(DISTINCT po.vendor_id) as total_vendors
|
||||||
`).
|
FROM inventory_movements im
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
LEFT JOIN purchase_orders po ON im.reference_id = po.id
|
||||||
Where("im.organization_id = ?", organizationID).
|
WHERE im.organization_id = ?
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
AND im.movement_type = ?
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
AND im.item_type = ?
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
AND im.reference_type = ?
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo)
|
AND im.created_at >= ? AND im.created_at <= ?
|
||||||
|
` + rawMaterialOutletFilter + `
|
||||||
|
),
|
||||||
|
non_inventory AS (
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(ei.amount), 0) as non_inventory_purchases,
|
||||||
|
COUNT(DISTINCT e.id) as non_inventory_expense_count
|
||||||
|
FROM expense_items ei
|
||||||
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
|
WHERE e.organization_id = ?
|
||||||
|
AND pc.type = ?
|
||||||
|
AND e.status = ?
|
||||||
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
|
` + nonInventoryOutletFilter + `
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rm.raw_material_purchases + ni.non_inventory_purchases as total_purchases,
|
||||||
|
rm.raw_material_purchases,
|
||||||
|
ni.non_inventory_purchases,
|
||||||
|
rm.raw_material_purchase_orders + ni.non_inventory_expense_count as total_purchase_orders,
|
||||||
|
rm.raw_material_purchase_orders,
|
||||||
|
ni.non_inventory_expense_count,
|
||||||
|
rm.total_quantity,
|
||||||
|
CASE
|
||||||
|
WHEN rm.raw_material_purchase_orders + ni.non_inventory_expense_count > 0
|
||||||
|
THEN (rm.raw_material_purchases + ni.non_inventory_purchases) / (rm.raw_material_purchase_orders + ni.non_inventory_expense_count)
|
||||||
|
ELSE 0
|
||||||
|
END as average_purchase_order_value,
|
||||||
|
rm.total_ingredients,
|
||||||
|
rm.total_vendors
|
||||||
|
FROM raw_material rm
|
||||||
|
CROSS JOIN non_inventory ni
|
||||||
|
`
|
||||||
|
|
||||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id")
|
if err := r.db.WithContext(ctx).Raw(summaryQuery, summaryArgs...).Scan(&summary).Error; err != nil {
|
||||||
|
|
||||||
if err := summaryQuery.Scan(&summary).Error; err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateFormat string
|
var dateFormat string
|
||||||
switch groupBy {
|
switch groupBy {
|
||||||
case "hour":
|
case "hour":
|
||||||
dateFormat = "DATE_TRUNC('hour', im.created_at)"
|
dateFormat = "DATE_TRUNC('hour', im.created_at)::timestamp"
|
||||||
case "week":
|
case "week":
|
||||||
dateFormat = "DATE_TRUNC('week', im.created_at)"
|
dateFormat = "DATE_TRUNC('week', im.created_at)::timestamp"
|
||||||
case "month":
|
case "month":
|
||||||
dateFormat = "DATE_TRUNC('month', im.created_at)"
|
dateFormat = "DATE_TRUNC('month', im.created_at)::timestamp"
|
||||||
default:
|
default:
|
||||||
dateFormat = "DATE_TRUNC('day', im.created_at)"
|
dateFormat = "DATE_TRUNC('day', im.created_at)::timestamp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonInventoryDateFormat := "DATE_TRUNC('day', e.transaction_date)::timestamp"
|
||||||
|
switch groupBy {
|
||||||
|
case "hour":
|
||||||
|
nonInventoryDateFormat = "DATE_TRUNC('hour', e.transaction_date)::timestamp"
|
||||||
|
case "week":
|
||||||
|
nonInventoryDateFormat = "DATE_TRUNC('week', e.transaction_date)::timestamp"
|
||||||
|
case "month":
|
||||||
|
nonInventoryDateFormat = "DATE_TRUNC('month', e.transaction_date)::timestamp"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMaterialDataArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.InventoryMovementTypePurchase,
|
||||||
|
"INGREDIENT",
|
||||||
|
entities.InventoryMovementReferenceTypePurchaseOrder,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
nonInventoryDataArgs := []interface{}{
|
||||||
|
organizationID,
|
||||||
|
entities.PurchaseCategoryTypeNonInventory,
|
||||||
|
"approved",
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
}
|
||||||
|
if outletID != nil {
|
||||||
|
rawMaterialDataArgs = append(rawMaterialDataArgs, *outletID)
|
||||||
|
nonInventoryDataArgs = append(nonInventoryDataArgs, *outletID)
|
||||||
|
}
|
||||||
|
dataArgs := append(rawMaterialDataArgs, nonInventoryDataArgs...)
|
||||||
|
|
||||||
var data []entities.PurchasingAnalyticsData
|
var data []entities.PurchasingAnalyticsData
|
||||||
dataQuery := r.db.WithContext(ctx).
|
dataQuery := `
|
||||||
Table("inventory_movements im").
|
WITH raw_material AS (
|
||||||
Select(`
|
SELECT
|
||||||
`+dateFormat+` as date,
|
` + dateFormat + ` as date,
|
||||||
COALESCE(SUM(im.total_cost), 0) as purchases,
|
COALESCE(SUM(im.total_cost), 0) as raw_material_purchases,
|
||||||
COUNT(DISTINCT im.reference_id) as purchase_orders,
|
COUNT(DISTINCT im.reference_id) as raw_material_purchase_orders,
|
||||||
COALESCE(SUM(im.quantity), 0) as quantity,
|
COALESCE(SUM(im.quantity), 0) as quantity,
|
||||||
COUNT(DISTINCT im.item_id) as ingredients,
|
COUNT(DISTINCT im.item_id) as ingredients,
|
||||||
COUNT(DISTINCT po.vendor_id) as vendors
|
COUNT(DISTINCT po.vendor_id) as vendors
|
||||||
`).
|
FROM inventory_movements im
|
||||||
Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id").
|
LEFT JOIN purchase_orders po ON im.reference_id = po.id
|
||||||
Where("im.organization_id = ?", organizationID).
|
WHERE im.organization_id = ?
|
||||||
Where("im.movement_type = ?", entities.InventoryMovementTypePurchase).
|
AND im.movement_type = ?
|
||||||
Where("im.item_type = ?", "INGREDIENT").
|
AND im.item_type = ?
|
||||||
Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder).
|
AND im.reference_type = ?
|
||||||
Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo).
|
AND im.created_at >= ? AND im.created_at <= ?
|
||||||
Group(dateFormat).
|
` + rawMaterialOutletFilter + `
|
||||||
Order(dateFormat)
|
GROUP BY 1
|
||||||
|
),
|
||||||
|
non_inventory AS (
|
||||||
|
SELECT
|
||||||
|
` + nonInventoryDateFormat + ` as date,
|
||||||
|
COALESCE(SUM(ei.amount), 0) as non_inventory_purchases,
|
||||||
|
COUNT(DISTINCT e.id) as non_inventory_expense_count
|
||||||
|
FROM expense_items ei
|
||||||
|
JOIN expenses e ON ei.expense_id = e.id
|
||||||
|
JOIN purchase_categories pc ON ei.purchase_category_id = pc.id
|
||||||
|
WHERE e.organization_id = ?
|
||||||
|
AND pc.type = ?
|
||||||
|
AND e.status = ?
|
||||||
|
AND e.transaction_date >= ? AND e.transaction_date <= ?
|
||||||
|
` + nonInventoryOutletFilter + `
|
||||||
|
GROUP BY 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(rm.date, ni.date) as date,
|
||||||
|
COALESCE(rm.raw_material_purchases, 0) + COALESCE(ni.non_inventory_purchases, 0) as purchases,
|
||||||
|
COALESCE(rm.raw_material_purchases, 0) as raw_material_purchases,
|
||||||
|
COALESCE(ni.non_inventory_purchases, 0) as non_inventory_purchases,
|
||||||
|
COALESCE(rm.raw_material_purchase_orders, 0) + COALESCE(ni.non_inventory_expense_count, 0) as purchase_orders,
|
||||||
|
COALESCE(rm.raw_material_purchase_orders, 0) as raw_material_purchase_orders,
|
||||||
|
COALESCE(ni.non_inventory_expense_count, 0) as non_inventory_expense_count,
|
||||||
|
COALESCE(rm.quantity, 0) as quantity,
|
||||||
|
COALESCE(rm.ingredients, 0) as ingredients,
|
||||||
|
COALESCE(rm.vendors, 0) as vendors
|
||||||
|
FROM raw_material rm
|
||||||
|
FULL OUTER JOIN non_inventory ni ON rm.date = ni.date
|
||||||
|
ORDER BY date
|
||||||
|
`
|
||||||
|
|
||||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id")
|
if err := r.db.WithContext(ctx).Raw(dataQuery, dataArgs...).Scan(&data).Error; err != nil {
|
||||||
|
|
||||||
if err := dataQuery.Scan(&data).Error; err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -171,7 +171,11 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
data[i] = contract.PurchasingAnalyticsData{
|
data[i] = contract.PurchasingAnalyticsData{
|
||||||
Date: item.Date,
|
Date: item.Date,
|
||||||
Purchases: item.Purchases,
|
Purchases: item.Purchases,
|
||||||
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
||||||
|
NonInventoryPurchases: item.NonInventoryPurchases,
|
||||||
PurchaseOrders: item.PurchaseOrders,
|
PurchaseOrders: item.PurchaseOrders,
|
||||||
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
||||||
|
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Ingredients: item.Ingredients,
|
Ingredients: item.Ingredients,
|
||||||
Vendors: item.Vendors,
|
Vendors: item.Vendors,
|
||||||
@ -211,7 +215,11 @@ func PurchasingAnalyticsModelToContract(resp *models.PurchasingAnalyticsResponse
|
|||||||
GroupBy: resp.GroupBy,
|
GroupBy: resp.GroupBy,
|
||||||
Summary: contract.PurchasingSummary{
|
Summary: contract.PurchasingSummary{
|
||||||
TotalPurchases: resp.Summary.TotalPurchases,
|
TotalPurchases: resp.Summary.TotalPurchases,
|
||||||
|
RawMaterialPurchases: resp.Summary.RawMaterialPurchases,
|
||||||
|
NonInventoryPurchases: resp.Summary.NonInventoryPurchases,
|
||||||
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
TotalPurchaseOrders: resp.Summary.TotalPurchaseOrders,
|
||||||
|
RawMaterialPurchaseOrders: resp.Summary.RawMaterialPurchaseOrders,
|
||||||
|
NonInventoryExpenseCount: resp.Summary.NonInventoryExpenseCount,
|
||||||
TotalQuantity: resp.Summary.TotalQuantity,
|
TotalQuantity: resp.Summary.TotalQuantity,
|
||||||
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
AveragePurchaseOrderValue: resp.Summary.AveragePurchaseOrderValue,
|
||||||
TotalIngredients: resp.Summary.TotalIngredients,
|
TotalIngredients: resp.Summary.TotalIngredients,
|
||||||
|
|||||||
@ -52,17 +52,47 @@ func TestPurchasingAnalyticsContractToModelIgnoresInvalidOutlet(t *testing.T) {
|
|||||||
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
func TestPurchasingAnalyticsModelToContractCopiesOutletName(t *testing.T) {
|
||||||
outletID := uuid.New()
|
outletID := uuid.New()
|
||||||
outletName := "Main Outlet"
|
outletName := "Main Outlet"
|
||||||
|
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
result := PurchasingAnalyticsModelToContract(&models.PurchasingAnalyticsResponse{
|
||||||
OrganizationID: uuid.New(),
|
OrganizationID: uuid.New(),
|
||||||
OutletID: &outletID,
|
OutletID: &outletID,
|
||||||
OutletName: &outletName,
|
OutletName: &outletName,
|
||||||
|
Summary: models.PurchasingSummary{
|
||||||
|
TotalPurchases: 300,
|
||||||
|
RawMaterialPurchases: 125,
|
||||||
|
NonInventoryPurchases: 175,
|
||||||
|
TotalPurchaseOrders: 3,
|
||||||
|
RawMaterialPurchaseOrders: 1,
|
||||||
|
NonInventoryExpenseCount: 2,
|
||||||
|
},
|
||||||
|
Data: []models.PurchasingAnalyticsData{
|
||||||
|
{
|
||||||
|
Date: now,
|
||||||
|
Purchases: 300,
|
||||||
|
RawMaterialPurchases: 125,
|
||||||
|
NonInventoryPurchases: 175,
|
||||||
|
PurchaseOrders: 3,
|
||||||
|
RawMaterialPurchaseOrders: 1,
|
||||||
|
NonInventoryExpenseCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NotNil(t, result)
|
require.NotNil(t, result)
|
||||||
require.Equal(t, &outletID, result.OutletID)
|
require.Equal(t, &outletID, result.OutletID)
|
||||||
require.NotNil(t, result.OutletName)
|
require.NotNil(t, result.OutletName)
|
||||||
require.Equal(t, outletName, *result.OutletName)
|
require.Equal(t, outletName, *result.OutletName)
|
||||||
|
require.Equal(t, float64(300), result.Summary.TotalPurchases)
|
||||||
|
require.Equal(t, float64(125), result.Summary.RawMaterialPurchases)
|
||||||
|
require.Equal(t, float64(175), result.Summary.NonInventoryPurchases)
|
||||||
|
require.Equal(t, int64(3), result.Summary.TotalPurchaseOrders)
|
||||||
|
require.Equal(t, int64(1), result.Summary.RawMaterialPurchaseOrders)
|
||||||
|
require.Equal(t, int64(2), result.Summary.NonInventoryExpenseCount)
|
||||||
|
require.Len(t, result.Data, 1)
|
||||||
|
require.Equal(t, float64(300), result.Data[0].Purchases)
|
||||||
|
require.Equal(t, float64(125), result.Data[0].RawMaterialPurchases)
|
||||||
|
require.Equal(t, float64(175), result.Data[0].NonInventoryPurchases)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
func TestPurchasingAnalyticsModelToContractOmitsNilOutletName(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user