更新之后,我也不知道有没有问题
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+60
-143
@@ -9,6 +9,12 @@ import logging
|
||||
from typing import Dict, Tuple, Optional, Any, List, Union
|
||||
|
||||
from ..utils.log_utils import get_logger
|
||||
from .handlers.barcode_mapper import BarcodeMapper
|
||||
from .handlers.unit_converter_handlers import (
|
||||
JianUnitHandler, BoxUnitHandler, TiHeUnitHandler,
|
||||
GiftUnitHandler, UnitHandler
|
||||
)
|
||||
from .validators import ProductValidator
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -93,7 +99,28 @@ class UnitConverter:
|
||||
# "xxmL*1"或"xx毫升*1"格式
|
||||
(r'([\d\.]+)(?:mL|毫升)[*xX×]?(\d+)?', r'\1mL*\2' if r'\2' else r'\1mL*1'),
|
||||
]
|
||||
|
||||
|
||||
# 初始化处理程序
|
||||
self._init_handlers()
|
||||
|
||||
# 初始化验证器
|
||||
self.validator = ProductValidator()
|
||||
|
||||
def _init_handlers(self):
|
||||
"""
|
||||
初始化各种处理程序
|
||||
"""
|
||||
# 创建条码处理程序
|
||||
self.barcode_mapper = BarcodeMapper(self.special_barcodes)
|
||||
|
||||
# 创建单位处理程序列表,优先级从高到低
|
||||
self.unit_handlers: List[UnitHandler] = [
|
||||
GiftUnitHandler(), # 首先处理赠品,优先级最高
|
||||
JianUnitHandler(), # 处理"件"单位
|
||||
BoxUnitHandler(), # 处理"箱"单位
|
||||
TiHeUnitHandler() # 处理"提"和"盒"单位
|
||||
]
|
||||
|
||||
def extract_unit_from_quantity(self, quantity_str: str) -> Tuple[Optional[float], Optional[str]]:
|
||||
"""
|
||||
从数量字符串中提取单位
|
||||
@@ -368,103 +395,15 @@ class UnitConverter:
|
||||
logger.error(f"解析规格时出错: {e}")
|
||||
return 1, 1, None
|
||||
|
||||
def _process_standard_unit_conversion(self, product: Dict) -> Dict:
|
||||
"""
|
||||
处理标准单位转换(件、箱、提、盒等单位)
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
# 复制原始数据,避免修改原始字典
|
||||
result = product.copy()
|
||||
|
||||
unit = result.get('unit', '')
|
||||
quantity = result.get('quantity', 0)
|
||||
price = result.get('price', 0)
|
||||
specification = result.get('specification', '')
|
||||
|
||||
# 跳过无效数据
|
||||
if not specification:
|
||||
return result
|
||||
|
||||
# 解析规格信息
|
||||
level1, level2, level3 = self.parse_specification(specification)
|
||||
|
||||
# "件"单位处理
|
||||
if unit in ['件']:
|
||||
# 计算包装数量(二级*三级,如果无三级则仅二级)
|
||||
packaging_count = level2 * (level3 or 1)
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"件单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 件 -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
return result
|
||||
|
||||
# "箱"单位处理 - 与"件"单位处理相同
|
||||
if unit in ['箱']:
|
||||
# 计算包装数量
|
||||
packaging_count = level2 * (level3 or 1)
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"箱单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 箱 -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
return result
|
||||
|
||||
# "提"和"盒"单位处理
|
||||
if unit in ['提', '盒']:
|
||||
# 如果是三级规格,按件处理
|
||||
if level3 is not None:
|
||||
# 计算包装数量 - 只乘以最后一级数量
|
||||
packaging_count = level3
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"提/盒单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
else:
|
||||
# 如果是二级规格,保持不变
|
||||
logger.info(f"提/盒单位(二级规格)处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
|
||||
|
||||
return result
|
||||
|
||||
# 其他单位保持不变
|
||||
logger.info(f"其他单位处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
|
||||
return result
|
||||
|
||||
def process_unit_conversion(self, product: Dict) -> Dict:
|
||||
"""
|
||||
处理单位转换,按照以下规则:
|
||||
1. 特殊条码: 优先处理特殊条码
|
||||
2. "件"单位: 数量×包装数量, 单价÷包装数量, 单位转为"瓶"
|
||||
3. "箱"单位: 数量×包装数量, 单价÷包装数量, 单位转为"瓶"
|
||||
4. "提"和"盒"单位: 如果是三级规格, 按件处理; 如果是二级规格, 保持不变
|
||||
5. 其他单位: 保持不变
|
||||
2. 赠品处理: 对于赠品,维持数量转换但单价为0
|
||||
3. "件"单位: 数量×包装数量, 单价÷包装数量, 单位转为"瓶"
|
||||
4. "箱"单位: 数量×包装数量, 单价÷包装数量, 单位转为"瓶"
|
||||
5. "提"和"盒"单位: 如果是三级规格, 按件处理; 如果是二级规格, 保持不变
|
||||
6. 其他单位: 保持不变
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
@@ -472,62 +411,40 @@ class UnitConverter:
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
# 首先验证商品数据
|
||||
product = self.validator.validate_product(product)
|
||||
|
||||
# 复制原始数据,避免修改原始字典
|
||||
result = product.copy()
|
||||
|
||||
barcode = result.get('barcode', '')
|
||||
unit = result.get('unit', '')
|
||||
quantity = result.get('quantity', 0)
|
||||
price = result.get('price', 0)
|
||||
specification = result.get('specification', '')
|
||||
|
||||
# 跳过无效数据
|
||||
if not barcode or not quantity:
|
||||
if not barcode:
|
||||
return result
|
||||
|
||||
# 特殊条码处理
|
||||
if barcode in self.special_barcodes:
|
||||
special_config = self.special_barcodes[barcode]
|
||||
|
||||
# 处理条码映射情况
|
||||
if 'map_to' in special_config:
|
||||
new_barcode = special_config['map_to']
|
||||
logger.info(f"条码映射: {barcode} -> {new_barcode}")
|
||||
result['barcode'] = new_barcode
|
||||
# 如果只是条码映射且没有其他特殊处理,继续执行标准单位处理
|
||||
if len(special_config) == 2: # 只有map_to和description两个字段
|
||||
# 继续标准处理流程,不提前返回
|
||||
return self._process_standard_unit_conversion(result)
|
||||
|
||||
multiplier = special_config.get('multiplier', 1)
|
||||
target_unit = special_config.get('target_unit', '瓶')
|
||||
|
||||
# 数量乘以倍数
|
||||
new_quantity = quantity * multiplier
|
||||
|
||||
# 如果有单价,单价除以倍数
|
||||
new_price = price / multiplier if price else 0
|
||||
|
||||
# 如果有固定单价,优先使用
|
||||
if 'fixed_price' in special_config:
|
||||
new_price = special_config['fixed_price']
|
||||
logger.info(f"特殊条码({barcode})使用固定单价: {new_price}")
|
||||
|
||||
# 如果有固定规格,设置规格
|
||||
if 'specification' in special_config:
|
||||
result['specification'] = special_config['specification']
|
||||
# 解析规格以获取包装数量
|
||||
package_quantity = self.parse_specification(special_config['specification'])
|
||||
if package_quantity:
|
||||
result['package_quantity'] = package_quantity
|
||||
logger.info(f"特殊条码({barcode})使用固定规格: {special_config['specification']}, 包装数量={package_quantity}")
|
||||
|
||||
logger.info(f"特殊条码处理: {barcode}, 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> {target_unit}")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = target_unit
|
||||
return result
|
||||
# 先处理条码映射
|
||||
result = self.barcode_mapper.map_barcode(result)
|
||||
|
||||
# 没有特殊条码,使用标准单位处理
|
||||
return self._process_standard_unit_conversion(result)
|
||||
# 如果没有规格信息,无法进行单位转换
|
||||
if not specification:
|
||||
# 尝试从商品名称推断规格
|
||||
inferred_spec = self.infer_specification_from_name(result.get('name', ''))
|
||||
if inferred_spec:
|
||||
result['specification'] = inferred_spec
|
||||
logger.info(f"从商品名称推断规格: {result.get('name', '')} -> {inferred_spec}")
|
||||
else:
|
||||
return result
|
||||
|
||||
# 解析规格信息
|
||||
level1, level2, level3 = self.parse_specification(result.get('specification', ''))
|
||||
|
||||
# 使用单位处理程序处理单位转换
|
||||
for handler in self.unit_handlers:
|
||||
if handler.can_handle(result):
|
||||
return handler.handle(result, level1, level2, level3)
|
||||
|
||||
# 没有找到适用的处理程序,保持不变
|
||||
logger.info(f"其他单位处理: 保持原样 数量: {result.get('quantity', 0)}, 单价: {result.get('price', 0)}, 单位: {result.get('unit', '')}")
|
||||
return result
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
单位转换处理程序包
|
||||
-----------------
|
||||
提供单位转换和条码处理的各种处理程序
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
# 导出所有处理程序类
|
||||
from .barcode_mapper import BarcodeMapper
|
||||
from .unit_converter_handlers import JianUnitHandler, BoxUnitHandler, TiHeUnitHandler, GiftUnitHandler, UnitHandler
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
条码映射处理程序
|
||||
-------------
|
||||
处理特殊条码的映射和转换
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class BarcodeMapper:
|
||||
"""
|
||||
条码映射器:负责特殊条码的映射和处理
|
||||
"""
|
||||
|
||||
def __init__(self, special_barcodes: Dict[str, Dict[str, Any]]):
|
||||
"""
|
||||
初始化条码映射器
|
||||
|
||||
Args:
|
||||
special_barcodes: 特殊条码配置字典
|
||||
"""
|
||||
self.special_barcodes = special_barcodes or {}
|
||||
|
||||
def map_barcode(self, product: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
映射商品条码,处理特殊情况
|
||||
|
||||
Args:
|
||||
product: 包含条码的商品信息字典
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
result = product.copy()
|
||||
barcode = result.get('barcode', '')
|
||||
|
||||
# 如果条码不在特殊条码列表中,直接返回
|
||||
if not barcode or barcode not in self.special_barcodes:
|
||||
return result
|
||||
|
||||
special_config = self.special_barcodes[barcode]
|
||||
|
||||
# 处理条码映射
|
||||
if 'map_to' in special_config:
|
||||
new_barcode = special_config['map_to']
|
||||
logger.info(f"条码映射: {barcode} -> {new_barcode}")
|
||||
result['barcode'] = new_barcode
|
||||
|
||||
# 处理特殊倍数
|
||||
if 'multiplier' in special_config:
|
||||
multiplier = special_config.get('multiplier', 1)
|
||||
target_unit = special_config.get('target_unit', '瓶')
|
||||
|
||||
# 数量乘以倍数
|
||||
quantity = result.get('quantity', 0)
|
||||
new_quantity = quantity * multiplier
|
||||
|
||||
# 单价除以倍数
|
||||
price = result.get('price', 0)
|
||||
new_price = price / multiplier if price else 0
|
||||
|
||||
# 如果有固定单价,优先使用
|
||||
if 'fixed_price' in special_config:
|
||||
new_price = special_config['fixed_price']
|
||||
logger.info(f"特殊条码({barcode})使用固定单价: {new_price}")
|
||||
|
||||
# 如果有固定规格,设置规格
|
||||
if 'specification' in special_config:
|
||||
result['specification'] = special_config['specification']
|
||||
logger.info(f"特殊条码({barcode})使用固定规格: {special_config['specification']}")
|
||||
|
||||
logger.info(f"特殊条码处理: {barcode}, 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {result.get('unit', '')} -> {target_unit}")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = target_unit
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
单位转换处理程序
|
||||
-------------
|
||||
处理不同单位的转换逻辑
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional, Any, Tuple, Protocol
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UnitHandler(ABC):
|
||||
"""
|
||||
单位处理器基类:定义单位处理接口
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def can_handle(self, product: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查是否可以处理该商品
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
是否可以处理
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
处理单位转换
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
level1: 一级包装数量
|
||||
level2: 二级包装数量
|
||||
level3: 三级包装数量,可能为None
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class JianUnitHandler(UnitHandler):
|
||||
"""
|
||||
处理"件"单位的转换
|
||||
"""
|
||||
|
||||
def can_handle(self, product: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查是否可以处理该商品(单位为"件")
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
是否可以处理
|
||||
"""
|
||||
unit = product.get('unit', '')
|
||||
return unit == '件'
|
||||
|
||||
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
处理"件"单位转换:数量×包装数量,单价÷包装数量,单位转为"瓶"
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
level1: 一级包装数量
|
||||
level2: 二级包装数量
|
||||
level3: 三级包装数量,可能为None
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
result = product.copy()
|
||||
|
||||
quantity = result.get('quantity', 0)
|
||||
price = result.get('price', 0)
|
||||
|
||||
# 计算包装数量(二级*三级,如果无三级则仅二级)
|
||||
packaging_count = level2 * (level3 or 1)
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"件单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 件 -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class BoxUnitHandler(UnitHandler):
|
||||
"""
|
||||
处理"箱"单位的转换
|
||||
"""
|
||||
|
||||
def can_handle(self, product: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查是否可以处理该商品(单位为"箱")
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
是否可以处理
|
||||
"""
|
||||
unit = product.get('unit', '')
|
||||
return unit == '箱'
|
||||
|
||||
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
处理"箱"单位转换:数量×包装数量,单价÷包装数量,单位转为"瓶"
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
level1: 一级包装数量
|
||||
level2: 二级包装数量
|
||||
level3: 三级包装数量,可能为None
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
result = product.copy()
|
||||
|
||||
quantity = result.get('quantity', 0)
|
||||
price = result.get('price', 0)
|
||||
|
||||
# 计算包装数量(二级*三级,如果无三级则仅二级)
|
||||
packaging_count = level2 * (level3 or 1)
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"箱单位处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: 箱 -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class TiHeUnitHandler(UnitHandler):
|
||||
"""
|
||||
处理"提"和"盒"单位的转换
|
||||
"""
|
||||
|
||||
def can_handle(self, product: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查是否可以处理该商品(单位为"提"或"盒")
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
是否可以处理
|
||||
"""
|
||||
unit = product.get('unit', '')
|
||||
return unit in ['提', '盒']
|
||||
|
||||
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
处理"提"和"盒"单位转换:
|
||||
- 如果是三级规格,按件处理(数量×包装数量,单价÷包装数量,单位转为"瓶")
|
||||
- 如果是二级规格,保持不变
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
level1: 一级包装数量
|
||||
level2: 二级包装数量
|
||||
level3: 三级包装数量,可能为None
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
result = product.copy()
|
||||
|
||||
quantity = result.get('quantity', 0)
|
||||
price = result.get('price', 0)
|
||||
unit = result.get('unit', '')
|
||||
|
||||
# 如果是三级规格,按件处理
|
||||
if level3 is not None:
|
||||
# 计算包装数量 - 只乘以最后一级数量
|
||||
packaging_count = level3
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
# 单价÷包装数量
|
||||
new_price = price / packaging_count if price else 0
|
||||
|
||||
logger.info(f"提/盒单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: {price} -> {new_price}, 单位: {unit} -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['price'] = new_price
|
||||
result['unit'] = '瓶'
|
||||
else:
|
||||
# 如果是二级规格,保持不变
|
||||
logger.info(f"提/盒单位(二级规格)处理: 保持原样 数量: {quantity}, 单价: {price}, 单位: {unit}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class GiftUnitHandler(UnitHandler):
|
||||
"""
|
||||
处理赠品的特殊情况
|
||||
"""
|
||||
|
||||
def can_handle(self, product: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
检查是否可以处理该商品(是否为赠品)
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
|
||||
Returns:
|
||||
是否可以处理
|
||||
"""
|
||||
return product.get('is_gift', False) is True
|
||||
|
||||
def handle(self, product: Dict[str, Any], level1: int, level2: int, level3: Optional[int]) -> Dict[str, Any]:
|
||||
"""
|
||||
处理赠品的单位转换:
|
||||
- 对于件/箱单位,数量仍然需要转换,但赠品的单价保持为0
|
||||
|
||||
Args:
|
||||
product: 商品信息字典
|
||||
level1: 一级包装数量
|
||||
level2: 二级包装数量
|
||||
level3: 三级包装数量,可能为None
|
||||
|
||||
Returns:
|
||||
处理后的商品信息字典
|
||||
"""
|
||||
result = product.copy()
|
||||
|
||||
unit = result.get('unit', '')
|
||||
quantity = result.get('quantity', 0)
|
||||
|
||||
# 根据单位类型选择适当的包装数计算
|
||||
if unit in ['件', '箱']:
|
||||
# 计算包装数量(二级*三级,如果无三级则仅二级)
|
||||
packaging_count = level2 * (level3 or 1)
|
||||
|
||||
# 数量×包装数量
|
||||
new_quantity = quantity * packaging_count
|
||||
|
||||
logger.info(f"赠品{unit}单位处理: 数量: {quantity} -> {new_quantity}, 单价: 0, 单位: {unit} -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['unit'] = '瓶'
|
||||
elif unit in ['提', '盒'] and level3 is not None:
|
||||
# 对于三级规格的提/盒,类似件处理
|
||||
new_quantity = quantity * level3
|
||||
|
||||
logger.info(f"赠品{unit}单位(三级规格)处理: 数量: {quantity} -> {new_quantity}, 单价: 0, 单位: {unit} -> 瓶")
|
||||
|
||||
result['quantity'] = new_quantity
|
||||
result['unit'] = '瓶'
|
||||
else:
|
||||
# 其他情况保持不变
|
||||
logger.info(f"赠品{unit}单位处理: 保持原样 数量: {quantity}, 单价: 0, 单位: {unit}")
|
||||
|
||||
# 确保单价为0
|
||||
result['price'] = 0
|
||||
|
||||
return result
|
||||
@@ -295,8 +295,15 @@ class ExcelProcessor:
|
||||
if package_quantity:
|
||||
product['package_quantity'] = package_quantity
|
||||
logger.info(f"解析规格: {product['specification']} -> 包装数量={package_quantity}")
|
||||
elif column_mapping.get('specification') and not pd.isna(row[column_mapping['specification']]):
|
||||
# 添加这段逻辑以处理通过列映射找到的规格列
|
||||
product['specification'] = str(row[column_mapping['specification']])
|
||||
package_quantity = self.parse_specification(product['specification'])
|
||||
if package_quantity:
|
||||
product['package_quantity'] = package_quantity
|
||||
logger.info(f"从映射列解析规格: {product['specification']} -> 包装数量={package_quantity}")
|
||||
else:
|
||||
# 逻辑1: 如果规格为空,尝试从商品名称推断规格
|
||||
# 只有在无法从Excel获取规格时,才尝试从商品名称推断规格
|
||||
if product['name']:
|
||||
# 特殊处理:优先检查名称中是否包含"容量*数量"格式
|
||||
container_pattern = r'.*?(\d+(?:\.\d+)?)\s*(?:ml|[mM][lL]|[lL]|升|毫升)[*×xX](\d+).*'
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
单位转换器测试模块
|
||||
---------------
|
||||
测试单位转换和条码映射逻辑
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from typing import Dict, Any
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
|
||||
|
||||
from app.core.excel.converter import UnitConverter
|
||||
from app.core.excel.validators import ProductValidator
|
||||
|
||||
|
||||
class TestUnitConverter(unittest.TestCase):
|
||||
"""
|
||||
测试单位转换器功能
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
测试前的准备工作
|
||||
"""
|
||||
self.converter = UnitConverter()
|
||||
|
||||
def test_jian_unit_conversion(self):
|
||||
"""
|
||||
测试"件"单位的转换
|
||||
"""
|
||||
# 准备测试数据
|
||||
product = {
|
||||
'barcode': '6954767400129',
|
||||
'name': '美汁源果粒橙1.8L*8瓶',
|
||||
'specification': '1.8L*8',
|
||||
'quantity': 1.0,
|
||||
'unit': '件',
|
||||
'price': 65.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(result['quantity'], 8.0)
|
||||
self.assertEqual(result['price'], 8.125)
|
||||
self.assertEqual(result['unit'], '瓶')
|
||||
|
||||
def test_box_unit_conversion(self):
|
||||
"""
|
||||
测试"箱"单位的转换
|
||||
"""
|
||||
# 准备测试数据
|
||||
product = {
|
||||
'barcode': '6925303721244',
|
||||
'name': '统一鲜橙多2L*6瓶',
|
||||
'specification': '2L*6',
|
||||
'quantity': 1.0,
|
||||
'unit': '箱',
|
||||
'price': 43.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果
|
||||
self.assertEqual(result['quantity'], 6.0)
|
||||
self.assertEqual(result['price'], 7.1666666666666667)
|
||||
self.assertEqual(result['unit'], '瓶')
|
||||
|
||||
def test_tihe_unit_conversion_level3(self):
|
||||
"""
|
||||
测试"提"单位的转换(三级规格)
|
||||
"""
|
||||
# 准备测试数据(三级规格:1*6*4,表示1排6提,每提4瓶)
|
||||
product = {
|
||||
'barcode': '6921168509347',
|
||||
'name': '农夫山泉550ml*24瓶',
|
||||
'specification': '1*6*4',
|
||||
'quantity': 2.0,
|
||||
'unit': '提',
|
||||
'price': 16.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果:三级规格,提单位特殊处理,数量*最后一级
|
||||
self.assertEqual(result['quantity'], 8.0) # 2提 * 4瓶/提
|
||||
self.assertEqual(result['price'], 4.0) # 16元/提 ÷ 4瓶/提
|
||||
self.assertEqual(result['unit'], '瓶')
|
||||
|
||||
def test_tihe_unit_conversion_level2(self):
|
||||
"""
|
||||
测试"提"单位的转换(二级规格)
|
||||
"""
|
||||
# 准备测试数据(二级规格:1*4,表示每件4提)
|
||||
product = {
|
||||
'barcode': '6921168509347',
|
||||
'name': '农夫山泉550ml*4瓶',
|
||||
'specification': '1*4',
|
||||
'quantity': 5.0,
|
||||
'unit': '提',
|
||||
'price': 10.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果:二级规格,提单位保持不变
|
||||
self.assertEqual(result['quantity'], 5.0)
|
||||
self.assertEqual(result['price'], 10.0)
|
||||
self.assertEqual(result['unit'], '提')
|
||||
|
||||
def test_barcode_mapping(self):
|
||||
"""
|
||||
测试条码映射
|
||||
"""
|
||||
# 准备测试数据(使用需要被映射的条码)
|
||||
product = {
|
||||
'barcode': '6920584471055', # 这个条码应映射到6920584471017
|
||||
'name': '测试映射条码商品',
|
||||
'specification': '1*12',
|
||||
'quantity': 1.0,
|
||||
'unit': '件',
|
||||
'price': 60.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果:条码应该被映射
|
||||
self.assertEqual(result['barcode'], '6920584471017')
|
||||
self.assertEqual(result['quantity'], 12.0) # 同时处理件单位转换
|
||||
self.assertEqual(result['price'], 5.0) # 60元/件 ÷ 12瓶/件
|
||||
self.assertEqual(result['unit'], '瓶')
|
||||
|
||||
def test_special_barcode_multiplier(self):
|
||||
"""
|
||||
测试特殊条码的倍数处理
|
||||
"""
|
||||
# 准备测试数据(使用特殊条码)
|
||||
product = {
|
||||
'barcode': '6925019900087', # 特殊条码:数量*10,单位转瓶
|
||||
'name': '特殊条码商品',
|
||||
'specification': '1*10',
|
||||
'quantity': 2.0,
|
||||
'unit': '箱',
|
||||
'price': 100.0
|
||||
}
|
||||
|
||||
# 执行转换
|
||||
result = self.converter.process_unit_conversion(product)
|
||||
|
||||
# 验证结果:特殊条码乘数应该生效
|
||||
self.assertEqual(result['quantity'], 20.0) # 2箱 * 10倍数
|
||||
self.assertEqual(result['price'], 5.0) # 100元/箱 ÷ 10倍数/箱
|
||||
self.assertEqual(result['unit'], '瓶')
|
||||
|
||||
|
||||
class TestProductValidator(unittest.TestCase):
|
||||
"""
|
||||
测试商品数据验证器功能
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
测试前的准备工作
|
||||
"""
|
||||
self.validator = ProductValidator()
|
||||
|
||||
def test_validate_barcode(self):
|
||||
"""
|
||||
测试条码验证
|
||||
"""
|
||||
# 测试有效条码
|
||||
is_valid, barcode, error = self.validator.validate_barcode('6925303721244')
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(barcode, '6925303721244')
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试包含非数字字符的条码
|
||||
is_valid, barcode, error = self.validator.validate_barcode('6925303-721244')
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(barcode, '6925303721244')
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试5开头的条码修正
|
||||
is_valid, barcode, error = self.validator.validate_barcode('5925303721244')
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(barcode, '6925303721244')
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试过短的条码
|
||||
is_valid, barcode, error = self.validator.validate_barcode('12345')
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(barcode, '12345')
|
||||
self.assertIn("条码长度异常", error)
|
||||
|
||||
# 测试仓库标识
|
||||
is_valid, barcode, error = self.validator.validate_barcode('仓库')
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(barcode, '仓库')
|
||||
self.assertEqual(error, "条码为仓库标识")
|
||||
|
||||
# 测试空值
|
||||
is_valid, barcode, error = self.validator.validate_barcode(None)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(barcode, "")
|
||||
self.assertEqual(error, "条码为空")
|
||||
|
||||
def test_validate_quantity(self):
|
||||
"""
|
||||
测试数量验证
|
||||
"""
|
||||
# 测试有效数量
|
||||
is_valid, quantity, error = self.validator.validate_quantity(10)
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(quantity, 10.0)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试字符串数量
|
||||
is_valid, quantity, error = self.validator.validate_quantity("25.5")
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(quantity, 25.5)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试带单位的数量
|
||||
is_valid, quantity, error = self.validator.validate_quantity("30瓶")
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(quantity, 30.0)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试零数量
|
||||
is_valid, quantity, error = self.validator.validate_quantity(0)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(quantity, 0.0)
|
||||
self.assertIn("数量必须大于0", error)
|
||||
|
||||
# 测试负数量
|
||||
is_valid, quantity, error = self.validator.validate_quantity(-5)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(quantity, 0.0)
|
||||
self.assertIn("数量必须大于0", error)
|
||||
|
||||
# 测试非数字
|
||||
is_valid, quantity, error = self.validator.validate_quantity("abc")
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(quantity, 0.0)
|
||||
self.assertIn("数量不包含数字", error)
|
||||
|
||||
# 测试空值
|
||||
is_valid, quantity, error = self.validator.validate_quantity(None)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(quantity, 0.0)
|
||||
self.assertEqual(error, "数量为空")
|
||||
|
||||
def test_validate_price(self):
|
||||
"""
|
||||
测试单价验证
|
||||
"""
|
||||
# 测试有效单价
|
||||
is_valid, price, is_gift, error = self.validator.validate_price(12.5)
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(price, 12.5)
|
||||
self.assertFalse(is_gift)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试字符串单价
|
||||
is_valid, price, is_gift, error = self.validator.validate_price("8.0")
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(price, 8.0)
|
||||
self.assertFalse(is_gift)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试零单价(赠品)
|
||||
is_valid, price, is_gift, error = self.validator.validate_price(0)
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(price, 0.0)
|
||||
self.assertTrue(is_gift)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试"赠品"标记
|
||||
is_valid, price, is_gift, error = self.validator.validate_price("赠品")
|
||||
self.assertTrue(is_valid)
|
||||
self.assertEqual(price, 0.0)
|
||||
self.assertTrue(is_gift)
|
||||
self.assertIsNone(error)
|
||||
|
||||
# 测试负单价
|
||||
is_valid, price, is_gift, error = self.validator.validate_price(-5)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(price, 0.0)
|
||||
self.assertTrue(is_gift)
|
||||
self.assertIn("单价不能为负数", error)
|
||||
|
||||
# 测试空值
|
||||
is_valid, price, is_gift, error = self.validator.validate_price(None)
|
||||
self.assertFalse(is_valid)
|
||||
self.assertEqual(price, 0.0)
|
||||
self.assertTrue(is_gift)
|
||||
self.assertEqual(error, "单价为空,视为赠品")
|
||||
|
||||
def test_validate_product(self):
|
||||
"""
|
||||
测试商品数据验证
|
||||
"""
|
||||
# 准备测试数据(有效商品)
|
||||
product = {
|
||||
'barcode': '6954767400129',
|
||||
'name': '测试商品',
|
||||
'specification': '1*12',
|
||||
'quantity': 3.0,
|
||||
'price': 36.0,
|
||||
'unit': '件',
|
||||
'is_gift': False
|
||||
}
|
||||
|
||||
# 验证有效商品
|
||||
result = self.validator.validate_product(product)
|
||||
self.assertEqual(result['barcode'], '6954767400129')
|
||||
self.assertEqual(result['quantity'], 3.0)
|
||||
self.assertEqual(result['price'], 36.0)
|
||||
self.assertFalse(result['is_gift'])
|
||||
|
||||
# 验证赠品商品
|
||||
gift_product = product.copy()
|
||||
gift_product['price'] = 0
|
||||
result = self.validator.validate_product(gift_product)
|
||||
self.assertEqual(result['price'], 0.0)
|
||||
self.assertTrue(result['is_gift'])
|
||||
|
||||
# 验证需要修复的商品
|
||||
invalid_product = {
|
||||
'barcode': '5954767-400129', # 需要修复前缀和移除非数字
|
||||
'name': '测试商品',
|
||||
'specification': '1*12',
|
||||
'quantity': '2件', # 需要提取数字
|
||||
'price': '赠品', # 赠品标记
|
||||
'unit': '件',
|
||||
'is_gift': False
|
||||
}
|
||||
|
||||
result = self.validator.validate_product(invalid_product)
|
||||
self.assertEqual(result['barcode'], '6954767400129') # 5->6,移除 '-'
|
||||
self.assertEqual(result['quantity'], 2.0) # 提取数字
|
||||
self.assertEqual(result['price'], 0.0) # 赠品价格为0
|
||||
self.assertTrue(result['is_gift']) # 标记为赠品
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
数据验证器模块
|
||||
----------
|
||||
提供对商品数据的验证和修复功能
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple, Union
|
||||
|
||||
from ..utils.log_utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ProductValidator:
|
||||
"""
|
||||
商品数据验证器:验证和修复商品数据
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
初始化商品数据验证器
|
||||
"""
|
||||
# 仓库标识列表
|
||||
self.warehouse_identifiers = ["仓库", "仓库全名", "warehouse"]
|
||||
|
||||
def validate_barcode(self, barcode: Any) -> Tuple[bool, str, Optional[str]]:
|
||||
"""
|
||||
验证并修复条码
|
||||
|
||||
Args:
|
||||
barcode: 原始条码值
|
||||
|
||||
Returns:
|
||||
(是否有效, 修复后的条码, 错误信息)元组
|
||||
"""
|
||||
error_message = None
|
||||
|
||||
# 处理空值
|
||||
if barcode is None:
|
||||
return False, "", "条码为空"
|
||||
|
||||
# 转为字符串
|
||||
barcode_str = str(barcode).strip()
|
||||
|
||||
# 处理"仓库"特殊情况
|
||||
if barcode_str in self.warehouse_identifiers:
|
||||
return False, barcode_str, "条码为仓库标识"
|
||||
|
||||
# 清理条码格式(移除非数字字符)
|
||||
barcode_clean = re.sub(r'\D', '', barcode_str)
|
||||
|
||||
# 如果清理后为空,无效
|
||||
if not barcode_clean:
|
||||
return False, barcode_str, "条码不包含数字"
|
||||
|
||||
# 对特定的错误条码进行修正(5开头改为6开头)
|
||||
if len(barcode_clean) > 8 and barcode_clean.startswith('5') and not barcode_clean.startswith('53'):
|
||||
original_barcode = barcode_clean
|
||||
barcode_clean = '6' + barcode_clean[1:]
|
||||
logger.info(f"修正条码前缀 5->6: {original_barcode} -> {barcode_clean}")
|
||||
|
||||
# 验证条码长度
|
||||
if len(barcode_clean) < 8 or len(barcode_clean) > 13:
|
||||
error_message = f"条码长度异常: {barcode_clean}, 长度={len(barcode_clean)}"
|
||||
logger.warning(error_message)
|
||||
return False, barcode_clean, error_message
|
||||
|
||||
# 验证条码是否全为数字
|
||||
if not barcode_clean.isdigit():
|
||||
error_message = f"条码包含非数字字符: {barcode_clean}"
|
||||
logger.warning(error_message)
|
||||
return False, barcode_clean, error_message
|
||||
|
||||
# 对于序号9的特殊情况,允许其条码格式
|
||||
if barcode_clean == "5321545613":
|
||||
logger.info(f"特殊条码验证通过: {barcode_clean}")
|
||||
return True, barcode_clean, None
|
||||
|
||||
logger.debug(f"条码验证通过: {barcode_clean}")
|
||||
return True, barcode_clean, None
|
||||
|
||||
def validate_quantity(self, quantity: Any) -> Tuple[bool, float, Optional[str]]:
|
||||
"""
|
||||
验证并修复数量
|
||||
|
||||
Args:
|
||||
quantity: 原始数量值
|
||||
|
||||
Returns:
|
||||
(是否有效, 修复后的数量, 错误信息)元组
|
||||
"""
|
||||
# 处理空值
|
||||
if quantity is None:
|
||||
return False, 0.0, "数量为空"
|
||||
|
||||
# 如果是字符串,尝试解析
|
||||
if isinstance(quantity, str):
|
||||
# 去除空白和非数字字符(保留小数点)
|
||||
quantity_clean = re.sub(r'[^\d\.]', '', quantity.strip())
|
||||
if not quantity_clean:
|
||||
return False, 0.0, "数量不包含数字"
|
||||
|
||||
try:
|
||||
quantity_value = float(quantity_clean)
|
||||
except ValueError:
|
||||
return False, 0.0, f"无法将数量 '{quantity}' 转换为数字"
|
||||
else:
|
||||
# 尝试直接转换
|
||||
try:
|
||||
quantity_value = float(quantity)
|
||||
except (ValueError, TypeError):
|
||||
return False, 0.0, f"无法将数量 '{quantity}' 转换为数字"
|
||||
|
||||
# 数量必须大于0
|
||||
if quantity_value <= 0:
|
||||
return False, 0.0, f"数量必须大于0,当前值: {quantity_value}"
|
||||
|
||||
return True, quantity_value, None
|
||||
|
||||
def validate_price(self, price: Any) -> Tuple[bool, float, bool, Optional[str]]:
|
||||
"""
|
||||
验证并修复单价
|
||||
|
||||
Args:
|
||||
price: 原始单价值
|
||||
|
||||
Returns:
|
||||
(是否有效, 修复后的单价, 是否为赠品, 错误信息)元组
|
||||
"""
|
||||
# 初始化不是赠品
|
||||
is_gift = False
|
||||
|
||||
# 处理空值
|
||||
if price is None:
|
||||
return False, 0.0, True, "单价为空,视为赠品"
|
||||
|
||||
# 如果是字符串,检查赠品标识
|
||||
if isinstance(price, str):
|
||||
price_str = price.strip().lower()
|
||||
if price_str in ["赠品", "gift", "赠送", "0", ""]:
|
||||
return True, 0.0, True, None
|
||||
|
||||
# 去除空白和非数字字符(保留小数点)
|
||||
price_clean = re.sub(r'[^\d\.]', '', price_str)
|
||||
if not price_clean:
|
||||
return False, 0.0, True, "单价不包含数字,视为赠品"
|
||||
|
||||
try:
|
||||
price_value = float(price_clean)
|
||||
except ValueError:
|
||||
return False, 0.0, True, f"无法将单价 '{price}' 转换为数字,视为赠品"
|
||||
else:
|
||||
# 尝试直接转换
|
||||
try:
|
||||
price_value = float(price)
|
||||
except (ValueError, TypeError):
|
||||
return False, 0.0, True, f"无法将单价 '{price}' 转换为数字,视为赠品"
|
||||
|
||||
# 单价为0视为赠品
|
||||
if price_value == 0:
|
||||
return True, 0.0, True, None
|
||||
|
||||
# 单价必须大于0
|
||||
if price_value < 0:
|
||||
return False, 0.0, True, f"单价不能为负数: {price_value},视为赠品"
|
||||
|
||||
return True, price_value, False, None
|
||||
|
||||
def validate_product(self, product: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
验证并修复商品数据
|
||||
|
||||
Args:
|
||||
product: 商品数据字典
|
||||
|
||||
Returns:
|
||||
修复后的商品数据字典
|
||||
"""
|
||||
# 创建新字典,避免修改原始数据
|
||||
validated_product = product.copy()
|
||||
|
||||
# 验证条码
|
||||
barcode = product.get('barcode', '')
|
||||
is_valid, fixed_barcode, error_msg = self.validate_barcode(barcode)
|
||||
if is_valid:
|
||||
validated_product['barcode'] = fixed_barcode
|
||||
else:
|
||||
logger.warning(f"条码验证失败: {error_msg}")
|
||||
if fixed_barcode:
|
||||
# 即使验证失败,但如果有修复后的条码仍然使用它
|
||||
validated_product['barcode'] = fixed_barcode
|
||||
|
||||
# 验证数量
|
||||
quantity = product.get('quantity', 0)
|
||||
is_valid, fixed_quantity, error_msg = self.validate_quantity(quantity)
|
||||
if is_valid:
|
||||
validated_product['quantity'] = fixed_quantity
|
||||
else:
|
||||
logger.warning(f"数量验证失败: {error_msg}")
|
||||
validated_product['quantity'] = 0.0
|
||||
|
||||
# 验证单价
|
||||
price = product.get('price', 0)
|
||||
is_valid, fixed_price, is_gift, error_msg = self.validate_price(price)
|
||||
validated_product['price'] = fixed_price
|
||||
|
||||
# 如果单价验证结果表示为赠品,更新赠品标识
|
||||
if is_gift:
|
||||
validated_product['is_gift'] = True
|
||||
if error_msg:
|
||||
logger.info(error_msg)
|
||||
|
||||
return validated_product
|
||||
Reference in New Issue
Block a user