mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
пересчет поправил редактирование с перепроведением галка автопроведения работает рекомендации починил
277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
package recommendations
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
|
|
"rmser/internal/domain/operations"
|
|
"rmser/internal/domain/recommendations"
|
|
)
|
|
|
|
type pgRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewRepository(db *gorm.DB) recommendations.Repository {
|
|
return &pgRepository{db: db}
|
|
}
|
|
|
|
// --- Методы Хранения ---
|
|
|
|
func (r *pgRepository) SaveAll(serverID uuid.UUID, items []recommendations.Recommendation) error {
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
// Удаляем только записи ЭТОГО сервера
|
|
if err := tx.Where("rms_server_id = ?", serverID).Delete(&recommendations.Recommendation{}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Проставляем server_id для всех записей
|
|
for i := range items {
|
|
items[i].RMSServerID = serverID
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
if err := tx.CreateInBatches(items, 100).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (r *pgRepository) GetAll(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
|
|
var items []recommendations.Recommendation
|
|
err := r.db.Where("rms_server_id = ?", serverID).Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
// --- Методы Аналитики ---
|
|
|
|
// 1. Товары (GOODS/PREPARED), не используемые в техкартах
|
|
func (r *pgRepository) FindUnusedGoods(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT
|
|
p.id as product_id,
|
|
p.name as product_name,
|
|
'Товар не используется ни в одной техкарте' as reason,
|
|
? as type
|
|
FROM products p
|
|
WHERE p.rms_server_id = ?
|
|
AND p.type IN ('GOODS', 'PREPARED')
|
|
AND p.is_deleted = false
|
|
AND p.id NOT IN (
|
|
SELECT DISTINCT ri.product_id FROM recipe_items ri
|
|
JOIN recipes r ON ri.recipe_id = r.id
|
|
WHERE r.rms_server_id = ?
|
|
)
|
|
AND p.id NOT IN (
|
|
SELECT DISTINCT r.product_id FROM recipes r
|
|
WHERE r.rms_server_id = ?
|
|
)
|
|
ORDER BY p.name ASC
|
|
`
|
|
|
|
if err := r.db.Raw(query, recommendations.TypeUnused, serverID, serverID, serverID).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// 2. Закупается, но нет в техкартах
|
|
func (r *pgRepository) FindPurchasedButUnused(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT DISTINCT
|
|
p.id as product_id,
|
|
p.name as product_name,
|
|
'Товар активно закупается, но не включен ни в одну техкарту' as reason,
|
|
? as type
|
|
FROM store_operations so
|
|
JOIN products p ON so.product_id = p.id
|
|
WHERE
|
|
so.rms_server_id = ?
|
|
AND so.op_type = ?
|
|
AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
AND p.is_deleted = false
|
|
AND p.id NOT IN (
|
|
SELECT DISTINCT ri.product_id FROM recipe_items ri
|
|
JOIN recipes r ON ri.recipe_id = r.id
|
|
WHERE r.rms_server_id = ?
|
|
)
|
|
ORDER BY p.name ASC
|
|
`
|
|
|
|
if err := r.db.Raw(query,
|
|
recommendations.TypePurchasedButUnused,
|
|
serverID,
|
|
operations.OpTypePurchase,
|
|
serverID,
|
|
).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// 3. Ингредиенты в актуальных техкартах без закупок
|
|
func (r *pgRepository) FindNoIncomingIngredients(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT
|
|
p.id as product_id,
|
|
p.name as product_name,
|
|
'Нет закупок (' || ? || ' дн). Входит в: ' || STRING_AGG(DISTINCT parent.name, ', ') as reason,
|
|
? as type
|
|
FROM recipe_items ri
|
|
JOIN recipes r ON ri.recipe_id = r.id
|
|
JOIN products p ON ri.product_id = p.id
|
|
JOIN products parent ON r.product_id = parent.id
|
|
WHERE
|
|
r.rms_server_id = ?
|
|
AND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
|
|
AND p.type = 'GOODS'
|
|
AND p.is_deleted = false
|
|
AND parent.is_deleted = false
|
|
AND p.id NOT IN (
|
|
SELECT so.product_id
|
|
FROM store_operations so
|
|
WHERE so.rms_server_id = ?
|
|
AND so.op_type = ?
|
|
AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
)
|
|
GROUP BY p.id, p.name
|
|
ORDER BY p.name ASC
|
|
`
|
|
|
|
if err := r.db.Raw(query,
|
|
strconv.Itoa(days),
|
|
recommendations.TypeNoIncoming,
|
|
serverID,
|
|
serverID,
|
|
operations.OpTypePurchase,
|
|
).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// 4. Товары, которые закупаем, но не расходуем ("Висяки")
|
|
func (r *pgRepository) FindStaleGoods(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT DISTINCT
|
|
p.id as product_id,
|
|
p.name as product_name,
|
|
? as reason,
|
|
? as type
|
|
FROM store_operations so
|
|
JOIN products p ON so.product_id = p.id
|
|
WHERE
|
|
so.rms_server_id = ?
|
|
AND so.op_type = ?
|
|
AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
AND p.is_deleted = false
|
|
AND p.id NOT IN (
|
|
SELECT so2.product_id
|
|
FROM store_operations so2
|
|
WHERE so2.rms_server_id = ?
|
|
AND so2.op_type = ?
|
|
AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
)
|
|
ORDER BY p.name ASC
|
|
`
|
|
|
|
reason := fmt.Sprintf("Были закупки, но нет расхода за %d дн.", days)
|
|
|
|
if err := r.db.Raw(query,
|
|
reason,
|
|
recommendations.TypeStale,
|
|
serverID,
|
|
operations.OpTypePurchase,
|
|
serverID,
|
|
operations.OpTypeUsage,
|
|
).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// 5. Блюдо используется в техкарте другого блюда
|
|
func (r *pgRepository) FindDishesInRecipes(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT DISTINCT
|
|
child.id as product_id,
|
|
child.name as product_name,
|
|
'Является Блюдом (DISH), но указан ингредиентом в: ' || parent.name as reason,
|
|
? as type
|
|
FROM recipe_items ri
|
|
JOIN products child ON ri.product_id = child.id
|
|
JOIN recipes r ON ri.recipe_id = r.id
|
|
JOIN products parent ON r.product_id = parent.id
|
|
WHERE
|
|
r.rms_server_id = ?
|
|
AND child.type = 'DISH'
|
|
AND child.is_deleted = false
|
|
AND parent.is_deleted = false
|
|
AND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
|
|
ORDER BY child.name ASC
|
|
`
|
|
|
|
if err := r.db.Raw(query, recommendations.TypeDishInRecipe, serverID).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// 6. Есть расход (Usage), но нет прихода (Purchase)
|
|
func (r *pgRepository) FindUsageWithoutPurchase(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
|
|
var results []recommendations.Recommendation
|
|
|
|
query := `
|
|
SELECT DISTINCT
|
|
p.id as product_id,
|
|
p.name as product_name,
|
|
? as reason,
|
|
? as type
|
|
FROM store_operations so
|
|
JOIN products p ON so.product_id = p.id
|
|
WHERE
|
|
so.rms_server_id = ?
|
|
AND so.op_type = ?
|
|
AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
AND p.type = 'GOODS'
|
|
AND p.is_deleted = false
|
|
AND p.id NOT IN (
|
|
SELECT so2.product_id
|
|
FROM store_operations so2
|
|
WHERE so2.rms_server_id = ?
|
|
AND so2.op_type = ?
|
|
AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day'
|
|
)
|
|
ORDER BY p.name ASC
|
|
`
|
|
|
|
reason := fmt.Sprintf("Товар расходуется (продажи/списания), но не закупался последние %d дн.", days)
|
|
|
|
if err := r.db.Raw(query,
|
|
reason,
|
|
recommendations.TypeUsageNoIncoming,
|
|
serverID,
|
|
operations.OpTypeUsage,
|
|
serverID,
|
|
operations.OpTypePurchase,
|
|
).Scan(&results).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return results, nil
|
|
}
|