fix: normalize warehouse codes for property inventory
This commit is contained in:
parent
e93c02d2ad
commit
5cfc056c6b
358
services/inventory_calc.py
Normal file
358
services/inventory_calc.py
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
"""库存计算相关函数"""
|
||||||
|
from services.db import query
|
||||||
|
from services.cache import RefreshableCache, register_cache
|
||||||
|
|
||||||
|
|
||||||
|
# ⚠️ 特殊成本价规则:以下商品不使用库存表中的成本价,使用单独指定的正确成本价。
|
||||||
|
# 键为商品编码,值为正确成本价。
|
||||||
|
SPECIAL_COST_PRICES = {
|
||||||
|
'POZ-FM-0184-TM-SM': 0.055, # 库存表55 → 正确0.055
|
||||||
|
'POZ-XFS-9014-TM-MD': 0.062, # 库存表31.32 → 正确0.062
|
||||||
|
'POZ-FM-9742-TM-MD': 0.181, # 库存表28.08 → 正确0.181
|
||||||
|
'POZ-MM-0580-TM-MD': 0.22, # 库存表26 → 正确0.22
|
||||||
|
'POZ-SKS-3374-LD': 0.1, # 库存表25 → 正确0.1
|
||||||
|
'POZ-SKS-3367-LD': 0.1, # 库存表25 → 正确0.1
|
||||||
|
'POZ-SKS-3350-LD': 0.1, # 库存表25 → 正确0.1
|
||||||
|
'POZ-SKS-3343-LD': 0.1, # 库存表25 → 正确0.1
|
||||||
|
}
|
||||||
|
# 以下商品的库存总数量不统计20号仓库。
|
||||||
|
EXCLUDE_WAREHOUSE_20_PRODUCTS = {
|
||||||
|
'POZ-03033',
|
||||||
|
'POZ-DXPW-1668-PDD',
|
||||||
|
'AH-YS-0584-TMGJ',
|
||||||
|
'POZ-JHY-7022',
|
||||||
|
'SEDATE-YMT-1682-PDD',
|
||||||
|
}
|
||||||
|
# 以下无产品资料商品固定归属成品,不按20号仓库自动归为包材。
|
||||||
|
FORCE_FINISHED_PRODUCTS = {
|
||||||
|
'POZ-WPH-0137',
|
||||||
|
'(AVA-YS-3252)',
|
||||||
|
'(UO-AJS-3362)',
|
||||||
|
'(WPK-CBZT-9707)',
|
||||||
|
'【UOUONE】LHJ0385',
|
||||||
|
}
|
||||||
|
FORCE_FINISHED_PRODUCTS_SQL = ', '.join(
|
||||||
|
"'%s'" % code.replace("'", "''") for code in sorted(FORCE_FINISHED_PRODUCTS)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cost_price(product_code, inventory_price):
|
||||||
|
"""解析产品成本价:命中特殊规则的用单独指定的正确成本价,否则用库存表成本价。"""
|
||||||
|
if product_code in SPECIAL_COST_PRICES:
|
||||||
|
return SPECIAL_COST_PRICES[product_code]
|
||||||
|
return inventory_price
|
||||||
|
|
||||||
|
|
||||||
|
def _get_latest_inv_date():
|
||||||
|
"""获取最新库存数据日期"""
|
||||||
|
latest = query("SELECT MAX(data_date) AS d FROM inventory_detail")[0]['d']
|
||||||
|
if hasattr(latest, 'strftime'):
|
||||||
|
return latest.strftime('%Y-%m-%d')
|
||||||
|
return str(latest)[:10]
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_product_inventory():
|
||||||
|
"""计算每个产品的最新库存(排除15号仓库)"""
|
||||||
|
latest_str = _get_latest_inv_date()
|
||||||
|
rows = query("""
|
||||||
|
SELECT product_code, SUM(available_qty) AS total_qty
|
||||||
|
FROM inventory_detail
|
||||||
|
WHERE data_date = %s AND warehouse_code != '15'
|
||||||
|
AND NOT (product_code IN ('POZ-03033', 'POZ-DXPW-1668-PDD', 'AH-YS-0584-TMGJ', 'POZ-JHY-7022', 'SEDATE-YMT-1682-PDD')
|
||||||
|
AND LPAD(warehouse_code, 2, '0') = '20')
|
||||||
|
GROUP BY product_code
|
||||||
|
""", (latest_str,))
|
||||||
|
|
||||||
|
inv_map = {}
|
||||||
|
for r in rows:
|
||||||
|
inv_map[r['product_code']] = int(r['total_qty']) if r['total_qty'] else 0
|
||||||
|
return inv_map
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_raw_inventory_15():
|
||||||
|
"""计算每个产品的最新加工厂库存(仅15号仓库)"""
|
||||||
|
latest_str = _get_latest_inv_date()
|
||||||
|
rows = query(
|
||||||
|
"SELECT product_code, SUM(available_qty) AS total_qty "
|
||||||
|
"FROM inventory_detail "
|
||||||
|
"WHERE data_date = %s AND warehouse_code = '15' "
|
||||||
|
"GROUP BY product_code",
|
||||||
|
(latest_str,))
|
||||||
|
inv15_map = {}
|
||||||
|
for r in rows:
|
||||||
|
inv15_map[r['product_code']] = int(r['total_qty']) if r['total_qty'] else 0
|
||||||
|
return inv15_map
|
||||||
|
|
||||||
|
# 缓存版获取最新加工厂库存
|
||||||
|
inv15_cache = RefreshableCache(maxsize=1)
|
||||||
|
register_cache(inv15_cache)
|
||||||
|
|
||||||
|
|
||||||
|
@inv15_cache.wrap
|
||||||
|
def compute_product_inventory_15():
|
||||||
|
"""缓存版加工厂库存计算(支持跨 worker 主动刷新)"""
|
||||||
|
return _compute_raw_inventory_15()
|
||||||
|
|
||||||
|
|
||||||
|
def compute_product_inventory_by_property(property_keyword):
|
||||||
|
"""根据仓库属性标签计算每个产品的库存(如云仓、外仓)"""
|
||||||
|
wh_codes = query(
|
||||||
|
"SELECT warehouse_code FROM warehouses WHERE FIND_IN_SET(%s, warehouse_property) > 0",
|
||||||
|
(property_keyword,)
|
||||||
|
)
|
||||||
|
# 仓库管理和库存明细的编号格式可能不同(例如管理表为035、明细为35)。
|
||||||
|
# 对纯数字仓号按数值匹配,其他编号仍按原字符串精确匹配。
|
||||||
|
numeric_codes = set()
|
||||||
|
text_codes = set()
|
||||||
|
for row in wh_codes:
|
||||||
|
code = str(row.get('warehouse_code') or '').strip()
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
if code.isdigit():
|
||||||
|
numeric_codes.add(int(code))
|
||||||
|
else:
|
||||||
|
text_codes.add(code)
|
||||||
|
if not numeric_codes and not text_codes:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
latest_str = _get_latest_inv_date()
|
||||||
|
match_clauses = []
|
||||||
|
params = [latest_str]
|
||||||
|
if numeric_codes:
|
||||||
|
placeholders = ','.join(['%s'] * len(numeric_codes))
|
||||||
|
match_clauses.append(
|
||||||
|
"warehouse_code REGEXP '^[0-9]+$' "
|
||||||
|
f"AND CAST(warehouse_code AS UNSIGNED) IN ({placeholders})"
|
||||||
|
)
|
||||||
|
params.extend(sorted(numeric_codes))
|
||||||
|
if text_codes:
|
||||||
|
placeholders = ','.join(['%s'] * len(text_codes))
|
||||||
|
match_clauses.append(f"warehouse_code IN ({placeholders})")
|
||||||
|
params.extend(sorted(text_codes))
|
||||||
|
|
||||||
|
rows = query(f"""
|
||||||
|
SELECT product_code, SUM(available_qty) AS total_qty
|
||||||
|
FROM inventory_detail
|
||||||
|
WHERE data_date = %s AND ({' OR '.join(match_clauses)})
|
||||||
|
GROUP BY product_code
|
||||||
|
""", tuple(params))
|
||||||
|
|
||||||
|
inv_map = {}
|
||||||
|
for r in rows:
|
||||||
|
inv_map[r['product_code']] = int(r['total_qty']) if r['total_qty'] else 0
|
||||||
|
return inv_map
|
||||||
|
|
||||||
|
# 缓存版云仓库存
|
||||||
|
_cloud_inv_cache = RefreshableCache(maxsize=1)
|
||||||
|
register_cache(_cloud_inv_cache)
|
||||||
|
|
||||||
|
|
||||||
|
@_cloud_inv_cache.wrap
|
||||||
|
def compute_cloud_inventory_cached():
|
||||||
|
"""缓存版云仓库存计算(支持跨 worker 主动刷新)"""
|
||||||
|
return compute_product_inventory_by_property('云仓')
|
||||||
|
|
||||||
|
|
||||||
|
# 缓存版外仓库存
|
||||||
|
_ext_inv_cache = RefreshableCache(maxsize=1)
|
||||||
|
register_cache(_ext_inv_cache)
|
||||||
|
|
||||||
|
|
||||||
|
@_ext_inv_cache.wrap
|
||||||
|
def compute_external_inventory_cached():
|
||||||
|
"""缓存版外仓库存计算(支持跨 worker 主动刷新)"""
|
||||||
|
return compute_product_inventory_by_property('外仓')
|
||||||
|
|
||||||
|
|
||||||
|
def compute_product_in_transit():
|
||||||
|
"""计算每个产品的最新生产在途、采购在途(所有仓库合计)"""
|
||||||
|
latest_str = _get_latest_inv_date()
|
||||||
|
rows = query(
|
||||||
|
"SELECT product_code, SUM(production_in_transit) AS prod_in_transit, "
|
||||||
|
"SUM(purchase_in_transit) AS purch_in_transit "
|
||||||
|
"FROM inventory_detail "
|
||||||
|
"WHERE data_date = %s "
|
||||||
|
"GROUP BY product_code",
|
||||||
|
(latest_str,))
|
||||||
|
transit_map = {}
|
||||||
|
for r in rows:
|
||||||
|
transit_map[r['product_code']] = {
|
||||||
|
'生产在途': int(r['prod_in_transit']) if r['prod_in_transit'] else 0,
|
||||||
|
'采购在途': int(r['purch_in_transit']) if r['purch_in_transit'] else 0,
|
||||||
|
}
|
||||||
|
return transit_map
|
||||||
|
|
||||||
|
# 缓存版在途库存
|
||||||
|
_transit_cache = RefreshableCache(maxsize=1)
|
||||||
|
register_cache(_transit_cache)
|
||||||
|
|
||||||
|
|
||||||
|
@_transit_cache.wrap
|
||||||
|
def compute_product_in_transit_cached():
|
||||||
|
"""缓存版在途库存计算(支持跨 worker 主动刷新)"""
|
||||||
|
return compute_product_in_transit()
|
||||||
|
|
||||||
|
|
||||||
|
def _determine_trend(avg_3, avg_7, avg_14):
|
||||||
|
"""根据3/7/14天销量均值判断销售趋势(共10种)"""
|
||||||
|
a3, a7, a14 = float(avg_3), float(avg_7), float(avg_14)
|
||||||
|
|
||||||
|
# 全部为0 → 近期无销售
|
||||||
|
if a3 == 0 and a7 == 0 and a14 == 0:
|
||||||
|
return '近期无销售'
|
||||||
|
|
||||||
|
# 防止除零
|
||||||
|
base_7 = a7 if a7 != 0 else 1
|
||||||
|
base_14 = a14 if a14 != 0 else 1
|
||||||
|
|
||||||
|
# 计算比值
|
||||||
|
ratio_3_7 = a3 / base_7
|
||||||
|
ratio_7_14 = a7 / base_14
|
||||||
|
diff_3_7 = abs(a3 - a7)
|
||||||
|
diff_7_14 = abs(a7 - a14)
|
||||||
|
|
||||||
|
# 应用趋势规则
|
||||||
|
if a3 > a7 and 0.95 <= ratio_7_14 <= 1.05:
|
||||||
|
return '开始上升'
|
||||||
|
if a3 < a7 and 0.95 <= ratio_7_14 <= 1.05:
|
||||||
|
return '开始下跌'
|
||||||
|
if diff_3_7 <= 20 and diff_7_14 <= 20:
|
||||||
|
return '平稳趋势'
|
||||||
|
if 0.95 <= ratio_3_7 <= 1.05 and a7 > a14:
|
||||||
|
return '上升趋稳'
|
||||||
|
if 0.95 <= ratio_3_7 <= 1.05 and a7 < a14:
|
||||||
|
return '下跌趋稳'
|
||||||
|
if a3 > a7 and a7 > a14:
|
||||||
|
return '持续上升'
|
||||||
|
if a3 < a7 and a7 > a14:
|
||||||
|
return '急跌'
|
||||||
|
if a3 < a7 and a7 < a14:
|
||||||
|
return '持续下跌'
|
||||||
|
if a3 > a7 and a7 < a14:
|
||||||
|
return '急增'
|
||||||
|
return '平稳趋势'
|
||||||
|
|
||||||
|
|
||||||
|
def _determine_trend_cached(avg_3, avg_7, avg_14):
|
||||||
|
"""趋势判断(带缓存)"""
|
||||||
|
return _determine_trend(avg_3, avg_7, avg_14)
|
||||||
|
|
||||||
|
|
||||||
|
# 主库存计算缓存
|
||||||
|
inv_cache = RefreshableCache(maxsize=1)
|
||||||
|
register_cache(inv_cache)
|
||||||
|
|
||||||
|
|
||||||
|
@inv_cache.wrap
|
||||||
|
def compute_product_inventory_cached():
|
||||||
|
"""缓存版库存计算(支持跨 worker 主动刷新)"""
|
||||||
|
return _compute_product_inventory()
|
||||||
|
|
||||||
|
|
||||||
|
def compute_inventory_cost_summary(latest_date=None):
|
||||||
|
"""按系统数据概览口径计算当前库存成本汇总(与 /api/dashboard/stats 一致)。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
- 成本价:命中 SPECIAL_COST_PRICES 的商品用单独指定的正确成本价,其余用库存表成本价
|
||||||
|
- 成本价与库存数量使用同一个核对日期(latest_date,默认最新库存日期)
|
||||||
|
- 排除内料(编码含 -NL 或名称含"内料")
|
||||||
|
- 成品/包材分类:有产品资料的按 product_type;无资料按仓库 20 号=包材、其他=成品
|
||||||
|
- 20号仓库特定商品(EXCLUDE_WAREHOUSE_20_PRODUCTS)不统计
|
||||||
|
- 生产在途按成品BOM中"内料"项的库存成本价计算;采购在途按成品成本价计算
|
||||||
|
返回 dict:total_cost / finished_cost / packaging_cost / 各分项 / latest_date
|
||||||
|
"""
|
||||||
|
if not latest_date:
|
||||||
|
latest_date = _get_latest_inv_date()
|
||||||
|
|
||||||
|
# 成本价(同一核对日期)
|
||||||
|
_latest_cost = query("""
|
||||||
|
SELECT product_code, ANY_VALUE(cost_price) AS pr
|
||||||
|
FROM inventory_detail WHERE data_date = %s GROUP BY product_code
|
||||||
|
""", (latest_date,))
|
||||||
|
_cost_map = {r['product_code']: resolve_cost_price(r['product_code'], float(r['pr']) if r['pr'] else 0) for r in _latest_cost}
|
||||||
|
|
||||||
|
# 成品/包材分类汇总
|
||||||
|
_inventory_rows = query("""
|
||||||
|
SELECT id.product_code,
|
||||||
|
SUM(CASE WHEN
|
||||||
|
(p.product_type IN ('成品', '清库存', '现货', '赠品') AND p.deleted_at IS NULL)
|
||||||
|
OR (p.product_code IS NULL AND (LPAD(id.warehouse_code, 2, '0') != '20'
|
||||||
|
OR id.product_code IN ('POZ-WPH-0137', '(AVA-YS-3252)', '(UO-AJS-3362)', '(WPK-CBZT-9707)', '【UOUONE】LHJ0385')))
|
||||||
|
THEN id.available_qty ELSE 0 END) AS finished_available_qty,
|
||||||
|
SUM(CASE WHEN
|
||||||
|
(p.product_type IN ('成品', '清库存', '现货', '赠品') AND p.deleted_at IS NULL)
|
||||||
|
OR (p.product_code IS NULL AND (LPAD(id.warehouse_code, 2, '0') != '20'
|
||||||
|
OR id.product_code IN ('POZ-WPH-0137', '(AVA-YS-3252)', '(UO-AJS-3362)', '(WPK-CBZT-9707)', '【UOUONE】LHJ0385')))
|
||||||
|
THEN id.production_in_transit ELSE 0 END) AS finished_production_in_transit,
|
||||||
|
SUM(CASE WHEN
|
||||||
|
(p.product_type IN ('成品', '清库存', '现货', '赠品') AND p.deleted_at IS NULL)
|
||||||
|
OR (p.product_code IS NULL AND (LPAD(id.warehouse_code, 2, '0') != '20'
|
||||||
|
OR id.product_code IN ('POZ-WPH-0137', '(AVA-YS-3252)', '(UO-AJS-3362)', '(WPK-CBZT-9707)', '【UOUONE】LHJ0385')))
|
||||||
|
THEN id.purchase_in_transit ELSE 0 END) AS finished_purchase_in_transit,
|
||||||
|
SUM(CASE WHEN
|
||||||
|
(p.product_type = '包材' AND p.product_name NOT LIKE %s AND p.deleted_at IS NULL)
|
||||||
|
OR (p.product_code IS NULL AND LPAD(id.warehouse_code, 2, '0') = '20'
|
||||||
|
AND id.product_code NOT IN ('POZ-WPH-0137', '(AVA-YS-3252)', '(UO-AJS-3362)', '(WPK-CBZT-9707)', '【UOUONE】LHJ0385'))
|
||||||
|
THEN id.available_qty ELSE 0 END) AS packaging_available_qty,
|
||||||
|
SUM(CASE WHEN
|
||||||
|
(p.product_type = '包材' AND p.product_name NOT LIKE %s AND p.deleted_at IS NULL)
|
||||||
|
OR (p.product_code IS NULL AND LPAD(id.warehouse_code, 2, '0') = '20'
|
||||||
|
AND id.product_code NOT IN ('POZ-WPH-0137', '(AVA-YS-3252)', '(UO-AJS-3362)', '(WPK-CBZT-9707)', '【UOUONE】LHJ0385'))
|
||||||
|
THEN id.purchase_in_transit ELSE 0 END) AS packaging_purchase_in_transit
|
||||||
|
FROM inventory_detail id
|
||||||
|
LEFT JOIN products p ON id.product_code = p.product_code
|
||||||
|
WHERE id.data_date = %s
|
||||||
|
AND id.product_code NOT LIKE %s
|
||||||
|
AND id.product_name NOT LIKE %s
|
||||||
|
AND NOT (id.product_code IN ('POZ-03033', 'POZ-DXPW-1668-PDD', 'AH-YS-0584-TMGJ', 'POZ-JHY-7022', 'SEDATE-YMT-1682-PDD')
|
||||||
|
AND LPAD(id.warehouse_code, 2, '0') = '20')
|
||||||
|
GROUP BY id.product_code
|
||||||
|
""", ('%内料%', '%内料%', latest_date, '%-NL%', '%内料%'))
|
||||||
|
|
||||||
|
# BOM 内料成本价(生产在途成本用)
|
||||||
|
_inner_cost_rows = query("""
|
||||||
|
SELECT b.product_code, b.material_code,
|
||||||
|
COALESCE(mi.cost_price, m.cost_price, 0) AS inner_cost
|
||||||
|
FROM bom b
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT product_code, ANY_VALUE(cost_price) AS cost_price
|
||||||
|
FROM inventory_detail WHERE data_date = %s GROUP BY product_code
|
||||||
|
) mi ON mi.product_code = b.material_code
|
||||||
|
LEFT JOIN products m ON m.product_code = b.material_code
|
||||||
|
WHERE (b.material_name LIKE %s OR b.material_code LIKE %s)
|
||||||
|
""", (latest_date, '%内料%', '%-NL'))
|
||||||
|
_inner_cost_map = {
|
||||||
|
r['product_code']: resolve_cost_price(r['material_code'], float(r['inner_cost'] or 0)) if r['material_code'] else 0
|
||||||
|
for r in _inner_cost_rows
|
||||||
|
}
|
||||||
|
|
||||||
|
finished_inventory_cost = finished_production_transit_cost = finished_purchase_transit_cost = 0.0
|
||||||
|
packaging_inventory_cost = packaging_purchase_transit_cost = 0.0
|
||||||
|
for r in _inventory_rows:
|
||||||
|
price = _cost_map.get(r['product_code'], 0)
|
||||||
|
finished_inventory_cost += float(r['finished_available_qty'] or 0) * price
|
||||||
|
finished_production_transit_cost += float(r['finished_production_in_transit'] or 0) * _inner_cost_map.get(r['product_code'], 0)
|
||||||
|
finished_purchase_transit_cost += float(r['finished_purchase_in_transit'] or 0) * price
|
||||||
|
packaging_inventory_cost += float(r['packaging_available_qty'] or 0) * price
|
||||||
|
packaging_purchase_transit_cost += float(r['packaging_purchase_in_transit'] or 0) * price
|
||||||
|
|
||||||
|
finished_inventory_cost = round(finished_inventory_cost, 2)
|
||||||
|
finished_production_transit_cost = round(finished_production_transit_cost, 2)
|
||||||
|
finished_purchase_transit_cost = round(finished_purchase_transit_cost, 2)
|
||||||
|
packaging_inventory_cost = round(packaging_inventory_cost, 2)
|
||||||
|
packaging_purchase_transit_cost = round(packaging_purchase_transit_cost, 2)
|
||||||
|
finished_cost = round(finished_inventory_cost + finished_production_transit_cost + finished_purchase_transit_cost, 2)
|
||||||
|
packaging_cost = round(packaging_inventory_cost + packaging_purchase_transit_cost, 2)
|
||||||
|
total_cost = round(finished_cost + packaging_cost, 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'latest_date': latest_date,
|
||||||
|
'total_cost': total_cost,
|
||||||
|
'finished_cost': finished_cost,
|
||||||
|
'packaging_cost': packaging_cost,
|
||||||
|
'finished_inventory_cost': finished_inventory_cost,
|
||||||
|
'finished_production_transit_cost': finished_production_transit_cost,
|
||||||
|
'finished_purchase_transit_cost': finished_purchase_transit_cost,
|
||||||
|
'packaging_inventory_cost': packaging_inventory_cost,
|
||||||
|
'packaging_purchase_transit_cost': packaging_purchase_transit_cost,
|
||||||
|
}
|
||||||
47
tests/test_inventory_property_mapping.py
Normal file
47
tests/test_inventory_property_mapping.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""仓库属性库存按编号规范化匹配的回归测试。"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from datetime import date
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from services import inventory_calc
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryPropertyMappingTest(unittest.TestCase):
|
||||||
|
def test_numeric_warehouse_codes_ignore_leading_zeroes(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
inventory_calc,
|
||||||
|
"query",
|
||||||
|
side_effect=[
|
||||||
|
[{"warehouse_code": "035"}],
|
||||||
|
[{"d": date(2026, 10, 9)}],
|
||||||
|
[{"product_code": "POZ-MSG-1322-PDD", "total_qty": 4935}],
|
||||||
|
],
|
||||||
|
) as query:
|
||||||
|
result = inventory_calc.compute_product_inventory_by_property("云仓")
|
||||||
|
|
||||||
|
self.assertEqual(4935, result["POZ-MSG-1322-PDD"])
|
||||||
|
inventory_sql, params = query.call_args.args
|
||||||
|
self.assertIn("CAST(warehouse_code AS UNSIGNED)", inventory_sql)
|
||||||
|
self.assertEqual(("2026-10-09", 35), params)
|
||||||
|
|
||||||
|
def test_non_numeric_warehouse_codes_remain_exact_matches(self):
|
||||||
|
with mock.patch.object(
|
||||||
|
inventory_calc,
|
||||||
|
"query",
|
||||||
|
side_effect=[
|
||||||
|
[{"warehouse_code": "OVERSEAS-A"}],
|
||||||
|
[{"d": date(2026, 10, 9)}],
|
||||||
|
[{"product_code": "SKU-1", "total_qty": 7}],
|
||||||
|
],
|
||||||
|
) as query:
|
||||||
|
result = inventory_calc.compute_product_inventory_by_property("外仓")
|
||||||
|
|
||||||
|
self.assertEqual(7, result["SKU-1"])
|
||||||
|
inventory_sql, params = query.call_args.args
|
||||||
|
self.assertNotIn("CAST(warehouse_code AS UNSIGNED)", inventory_sql)
|
||||||
|
self.assertEqual(("2026-10-09", "OVERSEAS-A"), params)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user