arc/hesabixAPI/tests/test_product_excel_import.py

243 lines
8.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""تست تمپلیت و نرمال‌سازی ایمپورت اکسل کالا."""
from io import BytesIO
from openpyxl import Workbook, load_workbook
from app.services.product_excel_import_normalize import (
build_create_payload,
build_update_payload,
is_sample_import_row,
map_headers,
parse_bool_strict,
parse_inventory_mode,
find_existing_product_in_index,
load_existing_product_match_index,
product_match_key,
provided_keys_from_raw,
select_products_import_worksheet,
)
from app.services.product_excel_import_opening_balance import (
OPENING_BALANCE_QUANTITY_KEY,
WarehouseImportIndex,
prepare_opening_balance_for_import_row,
)
from app.services.product_excel_import_spec import (
HEADER_ALIASES,
SAMPLE_PRODUCT_CODE,
USER_TEMPLATE_COLUMNS,
normalize_header,
)
from app.services.product_excel_import_template_service import (
ProductImportLookups,
build_products_import_workbook,
category_full_path,
)
def test_user_template_has_category_path_not_id():
keys = [c.key for c in USER_TEMPLATE_COLUMNS]
assert "category_path" in keys
assert "category_id" not in keys
assert "default_warehouse_id" not in keys
assert "tax_type_id" not in keys
assert "attribute_ids" not in keys
assert "general_barcodes" in keys
assert "is_active" in keys
assert "inventory_mode" in keys
assert "price_fx_currency_code" in keys
def test_header_aliases_prefer_path_over_id():
assert HEADER_ALIASES[normalize_header("مسیر دسته‌بندی")] == "category_path"
assert HEADER_ALIASES[normalize_header("شناسه دسته‌بندی")] == "category_id"
assert HEADER_ALIASES[normalize_header("بارکدهای عمومی")] == "general_barcodes"
assert HEADER_ALIASES[normalize_header("کد ارز قیمت")] == "price_fx_currency_code"
assert HEADER_ALIASES[normalize_header("فعال")] == "is_active"
def test_map_headers_localized():
mapped = map_headers(["کد", "نام", "مسیر دسته‌بندی", "بارکدهای عمومی"])
assert mapped == ["code", "name", "category_path", "general_barcodes"]
def test_parse_bool_empty_is_none():
assert parse_bool_strict("") is None
assert parse_bool_strict(None) is None
assert parse_bool_strict("بله") is True
assert parse_bool_strict("خیر") is False
def test_parse_inventory_mode():
assert parse_inventory_mode("فله‌ای") == "bulk"
assert parse_inventory_mode("unique") == "unique"
assert parse_inventory_mode("") is None
def test_sample_row_detection():
assert is_sample_import_row({"code": SAMPLE_PRODUCT_CODE, "name": "نمونه کالا"})
assert is_sample_import_row({"code": "X", "description": "ردیف نمونه — قبل از ایمپورت"})
assert not is_sample_import_row({"code": "P2", "name": "ماگ"})
def test_update_payload_skips_empty_cells():
item = {
"name": "ماگ",
"track_inventory": True,
"is_sales_taxable": None,
"category_id": 4,
"category_path": "مواد اولیه > پلاستیک",
"_row": 2,
}
provided = {"name", "track_inventory", "category_path"}
payload = build_update_payload(item, provided)
assert payload["name"] == "ماگ"
assert payload["track_inventory"] is True
assert payload["category_id"] == 4
assert "is_sales_taxable" not in payload
assert "category_path" not in payload
assert "_row" not in payload
def test_create_payload_drops_excel_only_and_empty():
item = {
"name": "ماگ",
"category_path": "پوشاک",
"category_id": 9,
"track_inventory": True,
"description": None,
"_provided_keys": {"name"},
}
payload = build_create_payload(item)
assert payload["name"] == "ماگ"
assert payload["category_id"] == 9
assert payload["track_inventory"] is True
assert "category_path" not in payload
assert "description" not in payload
assert "_provided_keys" not in payload
def test_select_products_sheet_skips_instructions():
wb = Workbook()
ws_help = wb.active
ws_help.title = "راهنما"
ws_help["A1"] = "راهنما"
ws_data = wb.create_sheet("کالاها")
ws_data["A1"] = "نام"
ws_data["B1"] = "کد"
assert select_products_import_worksheet(wb).title == "کالاها"
def test_template_workbook_has_lookups_and_no_category_id():
lookups = ProductImportLookups(
category_rows=[{"path": "مواد اولیه > پلاستیک", "title_fa": "پلاستیک", "title_en": "Plastics"}],
warehouses=[{"code": "WH1", "name": "اصلی", "is_default": True}],
tax_types=[{"code": "VAT", "title": "ارزش افزوده"}],
tax_units=[{"code": "U1", "name": "واحد ۱"}],
attributes=[{"title": "رنگ", "data_type": "text"}],
currencies=[{"code": "USD", "title": "دلار"}],
)
wb = build_products_import_workbook(lookups, "fa")
names = [ws.title for ws in wb.worksheets]
assert names[0] == "کالاها"
assert "راهنما" in names
assert "دسته‌بندی‌ها" in names
assert "انبارها" in names
headers = [c.value for c in wb["کالاها"][1]]
assert "مسیر دسته‌بندی" in headers
assert "شناسه دسته‌بندی" not in headers
assert "بارکدهای عمومی" in headers
assert "فعال" in headers
sample_name_idx = headers.index("نام") + 1
assert wb["کالاها"].cell(row=2, column=sample_name_idx).value == "نمونه کالا"
cat_headers = [c.value for c in wb["دسته‌بندی‌ها"][1]]
assert "مسیر دسته‌بندی" in cat_headers
assert wb["دسته‌بندی‌ها"].cell(row=2, column=1).value == "مواد اولیه > پلاستیک"
buf = BytesIO()
wb.save(buf)
buf.seek(0)
loaded = load_workbook(buf)
assert loaded["کالاها"]["A1"].value is not None
def test_empty_opening_balance_does_not_clear_on_update():
idx = WarehouseImportIndex([])
existing = type("P", (), {"id": 1, "default_warehouse_id": 3, "track_inventory": True})()
ob, errors, warnings, preview = prepare_opening_balance_for_import_row(
item={"item_type": "کالا", "track_inventory": True},
mapped_keys={OPENING_BALANCE_QUANTITY_KEY},
business_id=1,
db=None, # type: ignore[arg-type]
can_edit_opening_balance=True,
is_update=True,
existing_product=existing,
warehouse_index=idx,
)
assert ob is None
assert errors == []
assert preview == {}
def test_provided_keys_ignore_blank():
keys = provided_keys_from_raw({"name": "A", "code": " ", "track_inventory": True, "_row": 2})
assert keys == {"name", "track_inventory"}
def test_category_full_path_walks_parents():
parent = type("C", (), {"id": 1, "parent_id": None, "title_translations": {"fa": "مواد اولیه"}})()
child = type("C", (), {"id": 2, "parent_id": 1, "title_translations": {"fa": "پلاستیک"}})()
by_id = {1: parent, 2: child}
assert category_full_path(child, by_id, "fa") == "مواد اولیه > پلاستیک"
def test_import_match_index_resolves_without_database_lookup():
product = type("P", (), {"id": 7, "code": " P-1 ", "name": " ماگ بزرگ "})()
code_index = {"P-1": [product]}
found, error = find_existing_product_in_index(code_index, "code", {"code": "P-1"})
assert error is None
assert found is product
name_key = product_match_key("name", {"name": "ماگ بزرگ"})
found, error = find_existing_product_in_index({name_key: [product]}, "name", {"name": "ماگ بزرگ"})
assert error is None
assert found is product
def test_import_match_index_rejects_ambiguous_name():
p1 = type("P", (), {"id": 1})()
p2 = type("P", (), {"id": 2})()
found, error = find_existing_product_in_index(
{"ماگ": [p1, p2]},
"name",
{"name": "ماگ"},
)
assert found is None
assert error is not None
def test_existing_product_lookup_is_chunked_not_per_row():
class _Query:
def filter(self, *_args):
return self
def all(self):
return []
class _Session:
def __init__(self):
self.query_count = 0
def query(self, *_args):
self.query_count += 1
return _Query()
session = _Session()
index = load_existing_product_match_index( # type: ignore[arg-type]
session,
1,
"code",
[f"P-{i}" for i in range(1201)],
chunk_size=500,
)
assert index == {}
assert session.query_count == 3