feat: dual pricing policies + discount VAT splitting
Add production pricing policy (id_pol_productie) for articles with cont 341/345, smart discount VAT splitting across multiple rates, per-article id_pol support, and mapped SKU price validation. Settings UI updated with new controls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,18 +60,81 @@ def format_address_for_oracle(address: str, city: str, region: str) -> str:
|
||||
return f"JUD:{region_clean};{city_clean};{address_clean}"
|
||||
|
||||
|
||||
def compute_discount_split(order, settings: dict) -> dict | None:
|
||||
"""Compute proportional discount split by VAT rate from order items.
|
||||
|
||||
Returns: {"11": 3.98, "21": 1.43} or None if split not applicable.
|
||||
Only splits when split_discount_vat is enabled AND multiple VAT rates exist.
|
||||
When single VAT rate: returns {actual_rate: total} (smarter than GoMag's fixed 21%).
|
||||
"""
|
||||
if not order or order.discount_total <= 0:
|
||||
return None
|
||||
|
||||
split_enabled = settings.get("split_discount_vat") == "1"
|
||||
|
||||
# Calculate VAT distribution from order items (exclude zero-value)
|
||||
vat_totals = {}
|
||||
for item in order.items:
|
||||
item_value = abs(item.price * item.quantity)
|
||||
if item_value > 0:
|
||||
vat_key = str(int(item.vat)) if item.vat == int(item.vat) else str(item.vat)
|
||||
vat_totals[vat_key] = vat_totals.get(vat_key, 0) + item_value
|
||||
|
||||
if not vat_totals:
|
||||
return None
|
||||
|
||||
grand_total = sum(vat_totals.values())
|
||||
if grand_total <= 0:
|
||||
return None
|
||||
|
||||
if len(vat_totals) == 1:
|
||||
# Single VAT rate — use that rate (smarter than GoMag's fixed 21%)
|
||||
actual_vat = list(vat_totals.keys())[0]
|
||||
return {actual_vat: round(order.discount_total, 2)}
|
||||
|
||||
if not split_enabled:
|
||||
return None
|
||||
|
||||
# Multiple VAT rates — split proportionally
|
||||
result = {}
|
||||
discount_remaining = order.discount_total
|
||||
sorted_rates = sorted(vat_totals.keys(), key=lambda x: float(x))
|
||||
|
||||
for i, vat_rate in enumerate(sorted_rates):
|
||||
if i == len(sorted_rates) - 1:
|
||||
split_amount = round(discount_remaining, 2) # last gets remainder
|
||||
else:
|
||||
proportion = vat_totals[vat_rate] / grand_total
|
||||
split_amount = round(order.discount_total * proportion, 2)
|
||||
discount_remaining -= split_amount
|
||||
|
||||
if split_amount > 0:
|
||||
result[vat_rate] = split_amount
|
||||
|
||||
return result if result else None
|
||||
|
||||
|
||||
def build_articles_json(items, order=None, settings=None) -> str:
|
||||
"""Build JSON string for Oracle PACK_IMPORT_COMENZI.importa_comanda.
|
||||
Includes transport and discount as extra articles if configured."""
|
||||
Includes transport and discount as extra articles if configured.
|
||||
Supports per-article id_pol from codmat_policy_map and discount VAT splitting."""
|
||||
articles = []
|
||||
codmat_policy_map = settings.get("_codmat_policy_map", {}) if settings else {}
|
||||
default_id_pol = settings.get("id_pol", "") if settings else ""
|
||||
|
||||
for item in items:
|
||||
articles.append({
|
||||
article_dict = {
|
||||
"sku": item.sku,
|
||||
"quantity": str(item.quantity),
|
||||
"price": str(item.price),
|
||||
"vat": str(item.vat),
|
||||
"name": clean_web_text(item.name)
|
||||
})
|
||||
}
|
||||
# Per-article id_pol from dual-policy validation
|
||||
item_pol = codmat_policy_map.get(item.sku)
|
||||
if item_pol and str(item_pol) != str(default_id_pol):
|
||||
article_dict["id_pol"] = str(item_pol)
|
||||
articles.append(article_dict)
|
||||
|
||||
if order and settings:
|
||||
transport_codmat = settings.get("transport_codmat", "")
|
||||
@@ -90,20 +153,50 @@ def build_articles_json(items, order=None, settings=None) -> str:
|
||||
if settings.get("transport_id_pol"):
|
||||
article_dict["id_pol"] = settings["transport_id_pol"]
|
||||
articles.append(article_dict)
|
||||
# Discount total with quantity -1 (positive price)
|
||||
|
||||
# Discount — smart VAT splitting
|
||||
if order.discount_total > 0 and discount_codmat:
|
||||
# Use GoMag JSON discount VAT if available, fallback to settings
|
||||
discount_vat = getattr(order, 'discount_vat', None) or settings.get("discount_vat", "19")
|
||||
article_dict = {
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
"price": str(order.discount_total),
|
||||
"vat": discount_vat,
|
||||
"name": "Discount"
|
||||
}
|
||||
if settings.get("discount_id_pol"):
|
||||
article_dict["id_pol"] = settings["discount_id_pol"]
|
||||
articles.append(article_dict)
|
||||
discount_split = compute_discount_split(order, settings)
|
||||
|
||||
if discount_split and len(discount_split) > 1:
|
||||
# Multiple VAT rates — multiple discount lines
|
||||
for vat_rate, split_amount in sorted(discount_split.items(), key=lambda x: float(x[0])):
|
||||
article_dict = {
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
"price": str(split_amount),
|
||||
"vat": vat_rate,
|
||||
"name": f"Discount (TVA {vat_rate}%)"
|
||||
}
|
||||
if settings.get("discount_id_pol"):
|
||||
article_dict["id_pol"] = settings["discount_id_pol"]
|
||||
articles.append(article_dict)
|
||||
elif discount_split and len(discount_split) == 1:
|
||||
# Single VAT rate — use detected rate
|
||||
actual_vat = list(discount_split.keys())[0]
|
||||
article_dict = {
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
"price": str(order.discount_total),
|
||||
"vat": actual_vat,
|
||||
"name": "Discount"
|
||||
}
|
||||
if settings.get("discount_id_pol"):
|
||||
article_dict["id_pol"] = settings["discount_id_pol"]
|
||||
articles.append(article_dict)
|
||||
else:
|
||||
# Fallback — original behavior with GoMag VAT or settings default
|
||||
discount_vat = getattr(order, 'discount_vat', None) or settings.get("discount_vat", "21")
|
||||
article_dict = {
|
||||
"sku": discount_codmat,
|
||||
"quantity": "-1",
|
||||
"price": str(order.discount_total),
|
||||
"vat": discount_vat,
|
||||
"name": "Discount"
|
||||
}
|
||||
if settings.get("discount_id_pol"):
|
||||
article_dict["id_pol"] = settings["discount_id_pol"]
|
||||
articles.append(article_dict)
|
||||
|
||||
return json.dumps(articles)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
payment_method: str = None, delivery_method: str = None,
|
||||
order_total: float = None,
|
||||
delivery_cost: float = None, discount_total: float = None,
|
||||
web_status: str = None):
|
||||
web_status: str = None, discount_split: str = None):
|
||||
"""Upsert a single order — one row per order_number, status updated in place."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
@@ -71,8 +71,8 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||
last_sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total, web_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
delivery_cost, discount_total, web_status, discount_split)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(order_number) DO UPDATE SET
|
||||
customer_name = excluded.customer_name,
|
||||
status = CASE
|
||||
@@ -97,13 +97,14 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||
web_status = COALESCE(excluded.web_status, orders.web_status),
|
||||
discount_split = COALESCE(excluded.discount_split, orders.discount_split),
|
||||
updated_at = datetime('now')
|
||||
""", (order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message,
|
||||
json.dumps(missing_skus) if missing_skus else None,
|
||||
items_count, sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total, web_status))
|
||||
delivery_cost, discount_total, web_status, discount_split))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
@@ -142,8 +143,8 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
id_comanda, id_partener, error_message, missing_skus, items_count,
|
||||
last_sync_run_id, shipping_name, billing_name,
|
||||
payment_method, delivery_method, order_total,
|
||||
delivery_cost, discount_total, web_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
delivery_cost, discount_total, web_status, discount_split)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(order_number) DO UPDATE SET
|
||||
customer_name = excluded.customer_name,
|
||||
status = CASE
|
||||
@@ -168,6 +169,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
delivery_cost = COALESCE(excluded.delivery_cost, orders.delivery_cost),
|
||||
discount_total = COALESCE(excluded.discount_total, orders.discount_total),
|
||||
web_status = COALESCE(excluded.web_status, orders.web_status),
|
||||
discount_split = COALESCE(excluded.discount_split, orders.discount_split),
|
||||
updated_at = datetime('now')
|
||||
""", [
|
||||
(d["order_number"], d["order_date"], d["customer_name"], d["status"],
|
||||
@@ -178,7 +180,7 @@ async def save_orders_batch(orders_data: list[dict]):
|
||||
d.get("payment_method"), d.get("delivery_method"),
|
||||
d.get("order_total"),
|
||||
d.get("delivery_cost"), d.get("discount_total"),
|
||||
d.get("web_status"))
|
||||
d.get("web_status"), d.get("discount_split"))
|
||||
for d in orders_data
|
||||
])
|
||||
|
||||
|
||||
@@ -416,21 +416,86 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
pass
|
||||
elif item.sku in validation["direct"]:
|
||||
all_codmats.add(item.sku)
|
||||
# Get standard VAT rate from settings for PROC_TVAV metadata
|
||||
cota_tva = float(app_settings.get("discount_vat") or 21)
|
||||
|
||||
# Dual pricing policy support
|
||||
id_pol_productie = int(app_settings.get("id_pol_productie") or 0) or None
|
||||
codmat_policy_map = {}
|
||||
|
||||
if all_codmats:
|
||||
price_result = await asyncio.to_thread(
|
||||
validation_service.validate_prices, all_codmats, id_pol,
|
||||
conn, validation.get("direct_id_map")
|
||||
)
|
||||
if price_result["missing_price"]:
|
||||
logger.info(
|
||||
f"Auto-adding price 0 for {len(price_result['missing_price'])} "
|
||||
f"direct articles in policy {id_pol}"
|
||||
if id_pol_productie:
|
||||
# Dual-policy: classify articles by cont (sales vs production)
|
||||
codmat_policy_map = await asyncio.to_thread(
|
||||
validation_service.validate_and_ensure_prices_dual,
|
||||
all_codmats, id_pol, id_pol_productie,
|
||||
conn, validation.get("direct_id_map"),
|
||||
cota_tva=cota_tva
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
validation_service.ensure_prices,
|
||||
price_result["missing_price"], id_pol,
|
||||
_log_line(run_id,
|
||||
f"Politici duale: {sum(1 for v in codmat_policy_map.values() if v == id_pol)} vanzare, "
|
||||
f"{sum(1 for v in codmat_policy_map.values() if v == id_pol_productie)} productie")
|
||||
else:
|
||||
# Single-policy (backward compatible)
|
||||
price_result = await asyncio.to_thread(
|
||||
validation_service.validate_prices, all_codmats, id_pol,
|
||||
conn, validation.get("direct_id_map")
|
||||
)
|
||||
if price_result["missing_price"]:
|
||||
logger.info(
|
||||
f"Auto-adding price 0 for {len(price_result['missing_price'])} "
|
||||
f"direct articles in policy {id_pol}"
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
validation_service.ensure_prices,
|
||||
price_result["missing_price"], id_pol,
|
||||
conn, validation.get("direct_id_map"),
|
||||
cota_tva=cota_tva
|
||||
)
|
||||
|
||||
# Also validate mapped SKU prices (cherry-pick 1)
|
||||
mapped_skus_in_orders = set()
|
||||
for order in (truly_importable + already_in_roa):
|
||||
for item in order.items:
|
||||
if item.sku in validation["mapped"]:
|
||||
mapped_skus_in_orders.add(item.sku)
|
||||
|
||||
if mapped_skus_in_orders:
|
||||
mapped_codmat_data = await asyncio.to_thread(
|
||||
validation_service.resolve_mapped_codmats, mapped_skus_in_orders, conn
|
||||
)
|
||||
# Build id_map for mapped codmats and validate/ensure their prices
|
||||
mapped_id_map = {}
|
||||
for sku, entries in mapped_codmat_data.items():
|
||||
for entry in entries:
|
||||
mapped_id_map[entry["codmat"]] = {
|
||||
"id_articol": entry["id_articol"],
|
||||
"cont": entry.get("cont")
|
||||
}
|
||||
mapped_codmats = set(mapped_id_map.keys())
|
||||
if mapped_codmats:
|
||||
if id_pol_productie:
|
||||
mapped_policy_map = await asyncio.to_thread(
|
||||
validation_service.validate_and_ensure_prices_dual,
|
||||
mapped_codmats, id_pol, id_pol_productie,
|
||||
conn, mapped_id_map, cota_tva=cota_tva
|
||||
)
|
||||
codmat_policy_map.update(mapped_policy_map)
|
||||
else:
|
||||
mp_result = await asyncio.to_thread(
|
||||
validation_service.validate_prices,
|
||||
mapped_codmats, id_pol, conn, mapped_id_map
|
||||
)
|
||||
if mp_result["missing_price"]:
|
||||
await asyncio.to_thread(
|
||||
validation_service.ensure_prices,
|
||||
mp_result["missing_price"], id_pol,
|
||||
conn, mapped_id_map, cota_tva=cota_tva
|
||||
)
|
||||
|
||||
# Pass codmat_policy_map to import via app_settings
|
||||
if codmat_policy_map:
|
||||
app_settings["_codmat_policy_map"] = codmat_policy_map
|
||||
finally:
|
||||
await asyncio.to_thread(database.pool.release, conn)
|
||||
|
||||
@@ -529,6 +594,10 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
"cantitate_roa": None
|
||||
})
|
||||
|
||||
# Compute discount split for SQLite storage
|
||||
ds = import_service.compute_discount_split(order, app_settings)
|
||||
discount_split_json = json.dumps(ds) if ds else None
|
||||
|
||||
if result["success"]:
|
||||
imported_count += 1
|
||||
await sqlite_service.upsert_order(
|
||||
@@ -548,6 +617,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None,
|
||||
web_status=order.status or None,
|
||||
discount_split=discount_split_json,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED")
|
||||
# Store ROA address IDs (R9)
|
||||
@@ -577,6 +647,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
|
||||
delivery_cost=order.delivery_cost or None,
|
||||
discount_total=order.discount_total or None,
|
||||
web_status=order.status or None,
|
||||
discount_split=discount_split_json,
|
||||
)
|
||||
await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR")
|
||||
await sqlite_service.add_order_items(order.number, order_items_data)
|
||||
|
||||
@@ -28,10 +28,10 @@ def check_orders_in_roa(min_date, conn) -> dict:
|
||||
return existing
|
||||
|
||||
|
||||
def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, int]:
|
||||
"""Resolve CODMATs to best id_articol: prefers article with stock, then MAX(id_articol).
|
||||
def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, dict]:
|
||||
"""Resolve CODMATs to best id_articol + cont: prefers article with stock, then MAX(id_articol).
|
||||
Filters: sters=0 AND inactiv=0.
|
||||
Returns: {codmat: id_articol}
|
||||
Returns: {codmat: {"id_articol": int, "cont": str|None}}
|
||||
"""
|
||||
if not codmats:
|
||||
return {}
|
||||
@@ -58,8 +58,8 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
params["id_gestiune"] = id_gestiune
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT codmat, id_articol FROM (
|
||||
SELECT na.codmat, na.id_articol,
|
||||
SELECT codmat, id_articol, cont FROM (
|
||||
SELECT na.codmat, na.id_articol, na.cont,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY na.codmat
|
||||
ORDER BY
|
||||
@@ -79,7 +79,7 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
) WHERE rn = 1
|
||||
""", params)
|
||||
for row in cur:
|
||||
result[row[0]] = row[1]
|
||||
result[row[0]] = {"id_articol": row[1], "cont": row[2]}
|
||||
finally:
|
||||
if own_conn:
|
||||
database.pool.release(conn)
|
||||
@@ -90,11 +90,11 @@ def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) ->
|
||||
|
||||
def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
"""Validate a set of SKUs against Oracle.
|
||||
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: id_articol}}
|
||||
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: {"id_articol": int, "cont": str|None}}}
|
||||
- mapped: found in ARTICOLE_TERTI (active)
|
||||
- direct: found in NOM_ARTICOLE by codmat (not in ARTICOLE_TERTI)
|
||||
- missing: not found anywhere
|
||||
- direct_id_map: {codmat: id_articol} for direct SKUs (saves a round-trip in validate_prices)
|
||||
- direct_id_map: {codmat: {"id_articol": int, "cont": str|None}} for direct SKUs
|
||||
"""
|
||||
if not skus:
|
||||
return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
|
||||
@@ -129,6 +129,7 @@ def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
else:
|
||||
direct_id_map = {}
|
||||
direct = set()
|
||||
|
||||
finally:
|
||||
if own_conn:
|
||||
database.pool.release(conn)
|
||||
@@ -136,7 +137,8 @@ def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
|
||||
missing = skus - mapped - direct
|
||||
|
||||
logger.info(f"SKU validation: {len(mapped)} mapped, {len(direct)} direct, {len(missing)} missing")
|
||||
return {"mapped": mapped, "direct": direct, "missing": missing, "direct_id_map": direct_id_map}
|
||||
return {"mapped": mapped, "direct": direct, "missing": missing,
|
||||
"direct_id_map": direct_id_map}
|
||||
|
||||
def classify_orders(orders, validation_result):
|
||||
"""Classify orders as importable or skipped based on SKU validation.
|
||||
@@ -158,6 +160,19 @@ def classify_orders(orders, validation_result):
|
||||
|
||||
return importable, skipped
|
||||
|
||||
def _extract_id_map(direct_id_map: dict) -> dict:
|
||||
"""Extract {codmat: id_articol} from either enriched or simple format."""
|
||||
if not direct_id_map:
|
||||
return {}
|
||||
result = {}
|
||||
for cm, val in direct_id_map.items():
|
||||
if isinstance(val, dict):
|
||||
result[cm] = val["id_articol"]
|
||||
else:
|
||||
result[cm] = val
|
||||
return result
|
||||
|
||||
|
||||
def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None) -> dict:
|
||||
"""Check which CODMATs have a price entry in CRM_POLITICI_PRET_ART for the given policy.
|
||||
If direct_id_map is provided, skips the NOM_ARTICOLE lookup for those CODMATs.
|
||||
@@ -166,7 +181,7 @@ def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: di
|
||||
if not codmats:
|
||||
return {"has_price": set(), "missing_price": set()}
|
||||
|
||||
codmat_to_id = dict(direct_id_map) if direct_id_map else {}
|
||||
codmat_to_id = _extract_id_map(direct_id_map)
|
||||
ids_with_price = set()
|
||||
|
||||
own_conn = conn is None
|
||||
@@ -199,14 +214,18 @@ def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: di
|
||||
logger.info(f"Price validation (policy {id_pol}): {len(has_price)} have price, {len(missing_price)} missing price")
|
||||
return {"has_price": has_price, "missing_price": missing_price}
|
||||
|
||||
def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None):
|
||||
def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None,
|
||||
cota_tva: float = None):
|
||||
"""Insert price 0 entries for CODMATs missing from the given price policy.
|
||||
Uses batch executemany instead of individual INSERTs.
|
||||
Relies on TRG_CRM_POLITICI_PRET_ART trigger for ID_POL_ART sequence.
|
||||
cota_tva: VAT rate from settings (e.g. 21) — used for PROC_TVAV metadata.
|
||||
"""
|
||||
if not codmats:
|
||||
return
|
||||
|
||||
proc_tvav = 1 + (cota_tva / 100) if cota_tva else 1.21
|
||||
|
||||
own_conn = conn is None
|
||||
if own_conn:
|
||||
conn = database.get_oracle_connection()
|
||||
@@ -224,7 +243,7 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
|
||||
|
||||
# Build batch params using direct_id_map (already resolved via resolve_codmat_ids)
|
||||
batch_params = []
|
||||
codmat_id_map = dict(direct_id_map) if direct_id_map else {}
|
||||
codmat_id_map = _extract_id_map(direct_id_map)
|
||||
|
||||
for codmat in codmats:
|
||||
id_articol = codmat_id_map.get(codmat)
|
||||
@@ -234,7 +253,8 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
|
||||
batch_params.append({
|
||||
"id_pol": id_pol,
|
||||
"id_articol": id_articol,
|
||||
"id_valuta": id_valuta
|
||||
"id_valuta": id_valuta,
|
||||
"proc_tvav": proc_tvav
|
||||
})
|
||||
|
||||
if batch_params:
|
||||
@@ -244,9 +264,9 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
|
||||
ID_UTIL, DATAORA, PROC_TVAV, PRETFTVA, PRETCTVA)
|
||||
VALUES
|
||||
(:id_pol, :id_articol, 0, :id_valuta,
|
||||
-3, SYSDATE, 1.19, 0, 0)
|
||||
-3, SYSDATE, :proc_tvav, 0, 0)
|
||||
""", batch_params)
|
||||
logger.info(f"Batch inserted {len(batch_params)} price entries for policy {id_pol}")
|
||||
logger.info(f"Batch inserted {len(batch_params)} price entries for policy {id_pol} (PROC_TVAV={proc_tvav})")
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
@@ -254,3 +274,125 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
|
||||
database.pool.release(conn)
|
||||
|
||||
logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")
|
||||
|
||||
|
||||
def validate_and_ensure_prices_dual(codmats: set[str], id_pol_vanzare: int,
|
||||
id_pol_productie: int, conn, direct_id_map: dict,
|
||||
cota_tva: float = 21) -> dict[str, int]:
|
||||
"""Dual-policy price validation: assign each CODMAT to sales or production policy.
|
||||
|
||||
Logic:
|
||||
1. Check both policies in one SQL
|
||||
2. If article in one policy → use that
|
||||
3. If article in BOTH → prefer id_pol_vanzare
|
||||
4. If article in NEITHER → check cont: 341/345 → production, else → sales; insert price 0
|
||||
|
||||
Returns: codmat_policy_map = {codmat: assigned_id_pol}
|
||||
"""
|
||||
if not codmats:
|
||||
return {}
|
||||
|
||||
codmat_policy_map = {}
|
||||
id_map = _extract_id_map(direct_id_map)
|
||||
|
||||
# Collect all id_articol values we need to check
|
||||
id_to_codmats = {} # {id_articol: [codmat, ...]}
|
||||
for cm in codmats:
|
||||
aid = id_map.get(cm)
|
||||
if aid:
|
||||
id_to_codmats.setdefault(aid, []).append(cm)
|
||||
|
||||
if not id_to_codmats:
|
||||
return {}
|
||||
|
||||
# Query both policies in one SQL
|
||||
existing = {} # {id_articol: set of id_pol}
|
||||
id_list = list(id_to_codmats.keys())
|
||||
with conn.cursor() as cur:
|
||||
for i in range(0, len(id_list), 500):
|
||||
batch = id_list[i:i+500]
|
||||
placeholders = ",".join([f":a{j}" for j in range(len(batch))])
|
||||
params = {f"a{j}": aid for j, aid in enumerate(batch)}
|
||||
params["id_pol_v"] = id_pol_vanzare
|
||||
params["id_pol_p"] = id_pol_productie
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT pa.ID_ARTICOL, pa.ID_POL FROM CRM_POLITICI_PRET_ART pa
|
||||
WHERE pa.ID_POL IN (:id_pol_v, :id_pol_p) AND pa.ID_ARTICOL IN ({placeholders})
|
||||
""", params)
|
||||
for row in cur:
|
||||
existing.setdefault(row[0], set()).add(row[1])
|
||||
|
||||
# Classify each codmat
|
||||
missing_vanzare = set() # CODMATs needing price 0 in sales policy
|
||||
missing_productie = set() # CODMATs needing price 0 in production policy
|
||||
|
||||
for aid, cms in id_to_codmats.items():
|
||||
pols = existing.get(aid, set())
|
||||
for cm in cms:
|
||||
if pols:
|
||||
if id_pol_vanzare in pols:
|
||||
codmat_policy_map[cm] = id_pol_vanzare
|
||||
elif id_pol_productie in pols:
|
||||
codmat_policy_map[cm] = id_pol_productie
|
||||
else:
|
||||
# Not in any policy — classify by cont
|
||||
info = direct_id_map.get(cm, {})
|
||||
cont = info.get("cont", "") if isinstance(info, dict) else ""
|
||||
cont_str = str(cont or "").strip()
|
||||
if cont_str in ("341", "345"):
|
||||
codmat_policy_map[cm] = id_pol_productie
|
||||
missing_productie.add(cm)
|
||||
else:
|
||||
codmat_policy_map[cm] = id_pol_vanzare
|
||||
missing_vanzare.add(cm)
|
||||
|
||||
# Ensure prices for missing articles in each policy
|
||||
if missing_vanzare:
|
||||
ensure_prices(missing_vanzare, id_pol_vanzare, conn, direct_id_map, cota_tva=cota_tva)
|
||||
if missing_productie:
|
||||
ensure_prices(missing_productie, id_pol_productie, conn, direct_id_map, cota_tva=cota_tva)
|
||||
|
||||
logger.info(
|
||||
f"Dual-policy: {len(codmat_policy_map)} CODMATs assigned "
|
||||
f"(vanzare={sum(1 for v in codmat_policy_map.values() if v == id_pol_vanzare)}, "
|
||||
f"productie={sum(1 for v in codmat_policy_map.values() if v == id_pol_productie)})"
|
||||
)
|
||||
return codmat_policy_map
|
||||
|
||||
|
||||
def resolve_mapped_codmats(mapped_skus: set[str], conn) -> dict[str, list[dict]]:
|
||||
"""For mapped SKUs, get their underlying CODMATs from ARTICOLE_TERTI + nom_articole.
|
||||
|
||||
Returns: {sku: [{"codmat": str, "id_articol": int, "cont": str|None}]}
|
||||
"""
|
||||
if not mapped_skus:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
sku_list = list(mapped_skus)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
for i in range(0, len(sku_list), 500):
|
||||
batch = sku_list[i:i+500]
|
||||
placeholders = ",".join([f":s{j}" for j in range(len(batch))])
|
||||
params = {f"s{j}": sku for j, sku in enumerate(batch)}
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT at.sku, at.codmat, na.id_articol, na.cont
|
||||
FROM ARTICOLE_TERTI at
|
||||
JOIN NOM_ARTICOLE na ON na.codmat = at.codmat AND na.sters = 0 AND na.inactiv = 0
|
||||
WHERE at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0
|
||||
""", params)
|
||||
for row in cur:
|
||||
sku = row[0]
|
||||
if sku not in result:
|
||||
result[sku] = []
|
||||
result[sku].append({
|
||||
"codmat": row[1],
|
||||
"id_articol": row[2],
|
||||
"cont": row[3]
|
||||
})
|
||||
|
||||
logger.info(f"resolve_mapped_codmats: {len(result)} SKUs → {sum(len(v) for v in result.values())} CODMATs")
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user