diff --git a/brewman/alembic/versions/51013f52f0fb_add_exclude_active_normalized_name_.py b/brewman/alembic/versions/51013f52f0fb_add_exclude_active_normalized_name_.py index aa45e87f..82f30665 100644 --- a/brewman/alembic/versions/51013f52f0fb_add_exclude_active_normalized_name_.py +++ b/brewman/alembic/versions/51013f52f0fb_add_exclude_active_normalized_name_.py @@ -16,7 +16,7 @@ from alembic import op # revision identifiers, used by Alembic. revision = "51013f52f0fb" -down_revision = "9b4f7c2e81a3" +down_revision = "c7e1a94b2d30" branch_labels = None depends_on = None diff --git a/brewman/brewman/services/sales_import/products.py b/brewman/brewman/services/sales_import/products.py index cc3670ca..2e01488f 100644 --- a/brewman/brewman/services/sales_import/products.py +++ b/brewman/brewman/services/sales_import/products.py @@ -3,7 +3,7 @@ The mapping, once made, is standing truth: a Barker SKU whose mapping row exists is served from it verbatim, forever. The slugified product name is only the resolver for a Barker product never mapped before. Barker renames and units changes after first mapping are -ignored — brewman master data is edited by hand when that matters. +captured by creating a new product version or SKU version. Resolution for one Barker line on business date ``date_``: @@ -27,7 +27,7 @@ import re import uuid from dataclasses import dataclass -from datetime import date +from datetime import date, timedelta from decimal import Decimal from typing import Any, Literal @@ -87,6 +87,8 @@ def resolve_product(db: Session, line: BarkerSaleLine, date_: date) -> Provision """ binding = db.execute(select(BarkerProduct).where(BarkerProduct.barker_sku_id == line.sku_id)).scalar_one_or_none() if binding is not None: + _update_name_if_needed(db, binding.brewman_product_id, line.name, date_) + _update_units_if_needed(db, binding.brewman_sku_id, line.units, date_) return ProvisionedProduct( product_id=binding.brewman_product_id, sku_id=binding.brewman_sku_id, disposition="mapped" ) @@ -100,7 +102,9 @@ def resolve_product(db: Session, line: BarkerSaleLine, date_: date) -> Provision .first() ) if known_product_id is not None: + _update_name_if_needed(db, known_product_id, line.name, date_) sku_id = _find_or_create_sku(db, known_product_id, line, date_) + _update_units_if_needed(db, sku_id, line.units, date_) _write_mapping(db, line, known_product_id, sku_id) return ProvisionedProduct(product_id=known_product_id, sku_id=sku_id, disposition="linked") @@ -109,7 +113,10 @@ def resolve_product(db: Session, line: BarkerSaleLine, date_: date) -> Provision if product_id is None: product_id = _create_product(db, line, date_) disposition = "created" + else: + _update_name_if_needed(db, product_id, line.name, date_) sku_id = _find_or_create_sku(db, product_id, line, date_) + _update_units_if_needed(db, sku_id, line.units, date_) _write_mapping(db, line, product_id, sku_id) return ProvisionedProduct(product_id=product_id, sku_id=sku_id, disposition=disposition) @@ -178,24 +185,20 @@ def _find_product_id_by_name(db: Session, name: str, date_: date) -> uuid.UUID | """ wanted = _normalize(name) normalized_name = _pg_normalize(ProductVersion.name) - rows = ( - db.execute( - select(ProductVersion.product_id, ProductVersion.name).where( - normalized_name == wanted, - (ProductVersion.valid_from == None) # noqa: E711 - | (ProductVersion.valid_from <= date_), - (ProductVersion.valid_till == None) # noqa: E711 - | (ProductVersion.valid_till >= date_), - ) + rows = db.execute( + select(ProductVersion.product_id, ProductVersion.name).where( + normalized_name == wanted, + (ProductVersion.valid_from == None) # noqa: E711 + | (ProductVersion.valid_from <= date_), + (ProductVersion.valid_till == None) # noqa: E711 + | (ProductVersion.valid_till >= date_), ) - .scalars() - .all() - ) - if rows.count > 1: + ).all() + if len(rows) > 1: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Cannot import: '{name}' matches multiple products: " - + ", ".join(f"'{name}'" for (id_, name) in rows), + + ", ".join(f"'{p_name}'" for (id_, p_name) in rows), ) return rows[0][0] if rows else None @@ -323,3 +326,81 @@ def _write_mapping(db: Session, line: BarkerSaleLine, product_id: uuid.UUID, sku row.brewman_product_id = product_id row.brewman_sku_id = sku_id db.flush() # autoflush is off — later lookups must see this row + + +def _update_name_if_needed(db: Session, product_id: uuid.UUID, new_name: str, date_: date) -> None: + wanted = _normalize(new_name) + active_pv = db.execute( + select(ProductVersion).where( + ProductVersion.product_id == product_id, + (ProductVersion.valid_from == None) | (ProductVersion.valid_from <= date_), # noqa: E711 + (ProductVersion.valid_till == None) | (ProductVersion.valid_till >= date_), # noqa: E711 + ) + ).scalar_one_or_none() + + if active_pv is not None and _normalize(active_pv.name) != wanted: + if active_pv.valid_from == date_: + active_pv.name = new_name + active_pv.handle = ProductVersion.slugify(new_name) + db.flush() + else: + active_pv.valid_till = date_ - timedelta(days=1) + new_pv = ProductVersion( + product_id=active_pv.product_id, + name=new_name, + description=active_pv.description, + handle=ProductVersion.slugify(new_name), + fraction_units=active_pv.fraction_units, + product_group_id=active_pv.product_group_id, + account_id=active_pv.account_id, + is_purchased=active_pv.is_purchased, + is_sold=active_pv.is_sold, + allergen=active_pv.allergen, + protein=active_pv.protein, + carbohydrate=active_pv.carbohydrate, + total_sugar=active_pv.total_sugar, + added_sugar=active_pv.added_sugar, + total_fat=active_pv.total_fat, + saturated_fat=active_pv.saturated_fat, + trans_fat=active_pv.trans_fat, + cholestrol=active_pv.cholestrol, + sodium=active_pv.sodium, + msnf=active_pv.msnf, + other_solids=active_pv.other_solids, + total_solids=active_pv.total_solids, + water=active_pv.water, + valid_from=date_, + valid_till=None, + ) + db.add(new_pv) + db.flush() + + +def _update_units_if_needed(db: Session, sku_id: uuid.UUID, new_units: str, date_: date) -> None: + wanted = _normalize(new_units) + active_sv = db.execute( + select(SkuVersion).where( + SkuVersion.sku_id == sku_id, + (SkuVersion.valid_from == None) | (SkuVersion.valid_from <= date_), # noqa: E711 + (SkuVersion.valid_till == None) | (SkuVersion.valid_till >= date_), # noqa: E711 + ) + ).scalar_one_or_none() + + if active_sv is not None and _normalize(active_sv.units) != wanted: + if active_sv.valid_from == date_: + active_sv.units = new_units + db.flush() + else: + active_sv.valid_till = date_ - timedelta(days=1) + new_sv = SkuVersion( + sku_id=active_sv.sku_id, + units=new_units, + fraction=active_sv.fraction, + product_yield=active_sv.product_yield, + cost_price=active_sv.cost_price, + sale_price=active_sv.sale_price, + valid_from=date_, + valid_till=None, + ) + db.add(new_sv) + db.flush()