Sort order is no longer versioned. Also, data moved is not normalized to add the right skus to the products and coalesce contiguous products and skus with no changes.
This commit is contained in:
286
barker/alembic/versions/1e0daf6bc1ae_combine_skus.py
Normal file
286
barker/alembic/versions/1e0daf6bc1ae_combine_skus.py
Normal file
@ -0,0 +1,286 @@
|
||||
"""combine skus
|
||||
|
||||
Revision ID: 1e0daf6bc1ae
|
||||
Revises: c116517977af
|
||||
Create Date: 2026-01-23 15:56:01.354574
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1e0daf6bc1ae"
|
||||
down_revision = "c116517977af"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
-- Merge contiguous identical sku_versions (same business fields) into single rows.
|
||||
|
||||
-- 0) Work table: compute vf/vt sentinels + run_id (islands)
|
||||
CREATE TEMP TABLE tmp_sku_work ON COMMIT DROP AS
|
||||
WITH base AS (
|
||||
SELECT
|
||||
sv.id,
|
||||
sv.sku_id,
|
||||
sv.units,
|
||||
sv.fraction,
|
||||
sv.product_yield,
|
||||
sv.cost_price,
|
||||
sv.sale_price,
|
||||
sv.has_happy_hour,
|
||||
sv.menu_category_id,
|
||||
sv.valid_from,
|
||||
sv.valid_till,
|
||||
COALESCE(sv.valid_from, DATE '1900-01-01') AS vf,
|
||||
COALESCE(sv.valid_till, DATE '9999-12-31') AS vt
|
||||
FROM sku_versions sv
|
||||
),
|
||||
ordered AS (
|
||||
SELECT
|
||||
b.*,
|
||||
LAG(vt) OVER (
|
||||
PARTITION BY sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id
|
||||
ORDER BY vf, vt, id
|
||||
) AS prev_vt
|
||||
FROM base b
|
||||
),
|
||||
flagged AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE
|
||||
WHEN o.prev_vt IS NULL THEN 1
|
||||
WHEN o.vf = (o.prev_vt + INTERVAL '1 day')::date THEN 0
|
||||
ELSE 1
|
||||
END AS is_new_run
|
||||
FROM ordered o
|
||||
),
|
||||
runs AS (
|
||||
SELECT
|
||||
f.*,
|
||||
SUM(is_new_run) OVER (
|
||||
PARTITION BY sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id
|
||||
ORDER BY vf, vt, id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS run_id
|
||||
FROM flagged f
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id,
|
||||
valid_from, valid_till,
|
||||
vf, vt,
|
||||
run_id
|
||||
FROM runs;
|
||||
|
||||
-- 1) Survivors + merged validity (only runs with >1 row)
|
||||
CREATE TEMP TABLE tmp_sku_merge ON COMMIT DROP AS
|
||||
WITH run_stats AS (
|
||||
SELECT
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id,
|
||||
run_id,
|
||||
MIN(vf) AS min_vf,
|
||||
MAX(vt) AS max_vt,
|
||||
COUNT(*) AS cnt
|
||||
FROM tmp_sku_work
|
||||
GROUP BY
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id, run_id
|
||||
HAVING COUNT(*) > 1
|
||||
),
|
||||
survivors AS (
|
||||
SELECT DISTINCT ON (
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id
|
||||
)
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id,
|
||||
w.id AS survivor_id
|
||||
FROM tmp_sku_work w
|
||||
JOIN run_stats s
|
||||
ON s.sku_id = w.sku_id
|
||||
AND s.units = w.units
|
||||
AND s.fraction = w.fraction
|
||||
AND s.product_yield = w.product_yield
|
||||
AND s.cost_price = w.cost_price
|
||||
AND s.sale_price = w.sale_price
|
||||
AND s.has_happy_hour = w.has_happy_hour
|
||||
AND s.menu_category_id = w.menu_category_id
|
||||
AND s.run_id = w.run_id
|
||||
ORDER BY
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id,
|
||||
w.vf, w.vt, w.id
|
||||
)
|
||||
SELECT
|
||||
sv.survivor_id,
|
||||
sv.sku_id, sv.units, sv.fraction, sv.product_yield, sv.cost_price, sv.sale_price, sv.has_happy_hour, sv.menu_category_id, sv.run_id,
|
||||
CASE WHEN s.min_vf = DATE '1900-01-01' THEN NULL ELSE s.min_vf END AS new_valid_from,
|
||||
CASE WHEN s.max_vt = DATE '9999-12-31' THEN NULL ELSE s.max_vt END AS new_valid_till
|
||||
FROM survivors sv
|
||||
JOIN run_stats s
|
||||
ON s.sku_id = sv.sku_id
|
||||
AND s.units = sv.units
|
||||
AND s.fraction = sv.fraction
|
||||
AND s.product_yield = sv.product_yield
|
||||
AND s.cost_price = sv.cost_price
|
||||
AND s.sale_price = sv.sale_price
|
||||
AND s.has_happy_hour = sv.has_happy_hour
|
||||
AND s.menu_category_id = sv.menu_category_id
|
||||
AND s.run_id = sv.run_id;
|
||||
|
||||
-- 2) Rows to delete = everything in a merged run except the survivor
|
||||
CREATE TEMP TABLE tmp_sku_delete ON COMMIT DROP AS
|
||||
SELECT w.id
|
||||
FROM tmp_sku_work w
|
||||
JOIN tmp_sku_merge m
|
||||
ON m.sku_id = w.sku_id
|
||||
AND m.units = w.units
|
||||
AND m.fraction = w.fraction
|
||||
AND m.product_yield = w.product_yield
|
||||
AND m.cost_price = w.cost_price
|
||||
AND m.sale_price = w.sale_price
|
||||
AND m.has_happy_hour = w.has_happy_hour
|
||||
AND m.menu_category_id = w.menu_category_id
|
||||
AND m.run_id = w.run_id
|
||||
WHERE w.id <> m.survivor_id;
|
||||
|
||||
-- 3) Delete first (avoid exclusion overlap), then update survivor validity
|
||||
DELETE FROM sku_versions
|
||||
WHERE id IN (SELECT id FROM tmp_sku_delete);
|
||||
|
||||
UPDATE sku_versions sv
|
||||
SET valid_from = m.new_valid_from,
|
||||
valid_till = m.new_valid_till
|
||||
FROM tmp_sku_merge m
|
||||
WHERE sv.id = m.survivor_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
-- Merge contiguous identical product_versions (ignoring fraction_units)
|
||||
|
||||
-- 0) Work table: compute vf/vt sentinels + run_id (islands)
|
||||
CREATE TEMP TABLE tmp_pv_work ON COMMIT DROP AS
|
||||
WITH base AS (
|
||||
SELECT
|
||||
pv.id,
|
||||
pv.product_id,
|
||||
pv.name,
|
||||
pv.valid_from,
|
||||
pv.valid_till,
|
||||
COALESCE(pv.valid_from, DATE '1900-01-01') AS vf,
|
||||
COALESCE(pv.valid_till, DATE '9999-12-31') AS vt
|
||||
FROM product_versions pv
|
||||
),
|
||||
ordered AS (
|
||||
SELECT
|
||||
b.*,
|
||||
LAG(vt) OVER (
|
||||
PARTITION BY product_id, name
|
||||
ORDER BY vf, vt, id
|
||||
) AS prev_vt
|
||||
FROM base b
|
||||
),
|
||||
flagged AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE
|
||||
WHEN o.prev_vt IS NULL THEN 1
|
||||
WHEN o.vf = (o.prev_vt + INTERVAL '1 day')::date THEN 0
|
||||
ELSE 1
|
||||
END AS is_new_run
|
||||
FROM ordered o
|
||||
),
|
||||
runs AS (
|
||||
SELECT
|
||||
f.*,
|
||||
SUM(is_new_run) OVER (
|
||||
PARTITION BY product_id, name
|
||||
ORDER BY vf, vt, id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS run_id
|
||||
FROM flagged f
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
product_id,
|
||||
name,
|
||||
valid_from,
|
||||
valid_till,
|
||||
vf, vt,
|
||||
run_id
|
||||
FROM runs;
|
||||
|
||||
-- 1) Survivors + merged validity (only runs with >1 row)
|
||||
CREATE TEMP TABLE tmp_pv_merge ON COMMIT DROP AS
|
||||
WITH run_stats AS (
|
||||
SELECT
|
||||
product_id,
|
||||
name,
|
||||
run_id,
|
||||
MIN(vf) AS min_vf,
|
||||
MAX(vt) AS max_vt,
|
||||
COUNT(*) AS cnt
|
||||
FROM tmp_pv_work
|
||||
GROUP BY product_id, name, run_id
|
||||
HAVING COUNT(*) > 1
|
||||
),
|
||||
survivors AS (
|
||||
-- deterministic survivor: earliest vf, then earliest vt, then lowest id
|
||||
SELECT DISTINCT ON (w.product_id, w.name, w.run_id)
|
||||
w.product_id,
|
||||
w.name,
|
||||
w.run_id,
|
||||
w.id AS survivor_id
|
||||
FROM tmp_pv_work w
|
||||
JOIN run_stats s
|
||||
ON s.product_id = w.product_id
|
||||
AND s.name = w.name
|
||||
AND s.run_id = w.run_id
|
||||
ORDER BY w.product_id, w.name, w.run_id, w.vf, w.vt, w.id
|
||||
)
|
||||
SELECT
|
||||
sv.survivor_id,
|
||||
sv.product_id,
|
||||
sv.name,
|
||||
sv.run_id,
|
||||
CASE WHEN s.min_vf = DATE '1900-01-01' THEN NULL ELSE s.min_vf END AS new_valid_from,
|
||||
CASE WHEN s.max_vt = DATE '9999-12-31' THEN NULL ELSE s.max_vt END AS new_valid_till
|
||||
FROM survivors sv
|
||||
JOIN run_stats s
|
||||
ON s.product_id = sv.product_id
|
||||
AND s.name = sv.name
|
||||
AND s.run_id = sv.run_id;
|
||||
|
||||
-- 2) Rows to delete = everything in a merged run except the survivor
|
||||
CREATE TEMP TABLE tmp_pv_delete ON COMMIT DROP AS
|
||||
SELECT w.id
|
||||
FROM tmp_pv_work w
|
||||
JOIN tmp_pv_merge m
|
||||
ON m.product_id = w.product_id
|
||||
AND m.name = w.name
|
||||
AND m.run_id = w.run_id
|
||||
WHERE w.id <> m.survivor_id;
|
||||
|
||||
-- 3) Delete first (avoid exclusion overlap), then update survivor validity
|
||||
DELETE FROM product_versions
|
||||
WHERE id IN (SELECT id FROM tmp_pv_delete);
|
||||
|
||||
UPDATE product_versions pv
|
||||
SET valid_from = m.new_valid_from,
|
||||
valid_till = m.new_valid_till
|
||||
FROM tmp_pv_merge m
|
||||
WHERE pv.id = m.survivor_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@ -15,6 +15,14 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ---------------------------------------------------------------------
|
||||
# 0) Add NON-VERSIONED sort_order columns (but do NOT remove versioned ones here)
|
||||
# ---------------------------------------------------------------------
|
||||
op.add_column(
|
||||
"products",
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) stock_keeping_units
|
||||
# ---------------------------------------------------------------------
|
||||
@ -23,6 +31,7 @@ def upgrade():
|
||||
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("product_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("is_not_available", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["product_id"], ["products.id"], name=op.f("fk_stock_keeping_units_product_id_products")
|
||||
),
|
||||
@ -42,7 +51,6 @@ def upgrade():
|
||||
sa.Column("cost_price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("sale_price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("has_happy_hour", sa.Boolean(), nullable=False),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False),
|
||||
sa.Column("menu_category_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("valid_from", sa.Date(), nullable=True),
|
||||
sa.Column("valid_till", sa.Date(), nullable=True),
|
||||
@ -77,15 +85,17 @@ def upgrade():
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("product_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("is_not_available", sa.Boolean()),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
stock_keeping_units.insert().from_select(
|
||||
["id", "product_id", "is_not_available"],
|
||||
["id", "product_id", "is_not_available", "sort_order"],
|
||||
sa.select(
|
||||
sa.func.gen_random_uuid(),
|
||||
products.c.id,
|
||||
sa.false(),
|
||||
sa.literal(0),
|
||||
),
|
||||
)
|
||||
)
|
||||
@ -141,7 +151,8 @@ def upgrade():
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE stock_keeping_units sku
|
||||
SET is_not_available = pv.is_not_available
|
||||
SET is_not_available = pv.is_not_available,
|
||||
sort_order = pv.sort_order
|
||||
FROM product_versions pv
|
||||
WHERE pv.product_id = sku.product_id
|
||||
AND pv.valid_till IS NULL
|
||||
@ -160,7 +171,6 @@ def upgrade():
|
||||
"cost_price",
|
||||
"sale_price",
|
||||
"has_happy_hour",
|
||||
"sort_order",
|
||||
"menu_category_id",
|
||||
"valid_from",
|
||||
"valid_till",
|
||||
@ -168,13 +178,12 @@ def upgrade():
|
||||
sa.select(
|
||||
sa.func.gen_random_uuid(),
|
||||
stock_keeping_units.c.id,
|
||||
pv.c.units,
|
||||
sa.func.coalesce(sa.func.nullif(sa.func.btrim(pv.c.units), ""), sa.literal("pc")),
|
||||
sa.cast(1.0, sa.Numeric(15, 5)),
|
||||
sa.cast(1.0, sa.Numeric(15, 5)),
|
||||
sa.cast(0.0, sa.Numeric(15, 2)),
|
||||
sa.cast(pv.c.price, sa.Numeric(15, 2)),
|
||||
pv.c.has_happy_hour,
|
||||
pv.c.sort_order,
|
||||
pv.c.menu_category_id,
|
||||
pv.c.valid_from,
|
||||
pv.c.valid_till,
|
||||
@ -206,7 +215,7 @@ def upgrade():
|
||||
# ---------------------------------------------------------------------
|
||||
op.alter_column("product_versions", "units", new_column_name="fraction_units")
|
||||
op.drop_constraint(op.f("fk_products_menu_category_id_menu_categories"), "product_versions", type_="foreignkey")
|
||||
for col in ["price", "quantity", "has_happy_hour", "is_not_available", "menu_category_id"]:
|
||||
for col in ["price", "quantity", "has_happy_hour", "is_not_available", "menu_category_id", "sort_order"]:
|
||||
op.drop_column("product_versions", col)
|
||||
|
||||
|
||||
|
||||
146
barker/alembic/versions/c116517977af_normalize_sku.py
Normal file
146
barker/alembic/versions/c116517977af_normalize_sku.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""normalize sku
|
||||
|
||||
Revision ID: c116517977af
|
||||
Revises: 32c508eed4df
|
||||
Create Date: 2026-01-23 15:33:39.682395
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c116517977af"
|
||||
down_revision = "32c508eed4df"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) Build mapping: old_product_id -> canonical_product_id
|
||||
# Canonical = product whose CURRENT pv (valid_till IS NULL) has
|
||||
# latest valid_from (prefer newer). Tie-break: product_id.
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TEMP TABLE tmp_product_canon (
|
||||
old_product_id uuid PRIMARY KEY,
|
||||
canonical_product_id uuid NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH current_pv AS (
|
||||
SELECT
|
||||
pv.product_id,
|
||||
pv.name,
|
||||
pv.valid_from
|
||||
FROM product_versions pv
|
||||
WHERE pv.valid_till IS NULL
|
||||
),
|
||||
canon AS (
|
||||
SELECT DISTINCT ON (name)
|
||||
name,
|
||||
product_id AS canonical_product_id
|
||||
FROM current_pv
|
||||
ORDER BY name, valid_from DESC NULLS LAST, product_id
|
||||
)
|
||||
INSERT INTO tmp_product_canon (old_product_id, canonical_product_id)
|
||||
SELECT
|
||||
cp.product_id AS old_product_id,
|
||||
c.canonical_product_id
|
||||
FROM current_pv cp
|
||||
JOIN canon c ON c.name = cp.name;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 2) Move SKUs to canonical product
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE stock_keeping_units sku
|
||||
SET product_id = m.canonical_product_id
|
||||
FROM tmp_product_canon m
|
||||
WHERE sku.product_id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 3) Merge modifier_categories_products:
|
||||
# (A) delete duplicates that would conflict after update
|
||||
# (B) update remaining rows to canonical_product_id
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM modifier_categories_products mcp
|
||||
USING tmp_product_canon m
|
||||
WHERE m.old_product_id <> m.canonical_product_id
|
||||
AND mcp.product_id = m.old_product_id
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM modifier_categories_products mcp2
|
||||
WHERE mcp2.product_id = m.canonical_product_id
|
||||
AND mcp2.modifier_category_id = mcp.modifier_category_id
|
||||
);
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE modifier_categories_products mcp
|
||||
SET product_id = m.canonical_product_id
|
||||
FROM tmp_product_canon m
|
||||
WHERE m.old_product_id <> m.canonical_product_id
|
||||
AND mcp.product_id = m.old_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 4) Delete duplicate product_versions and products.
|
||||
# Since you said "newer products mean more than old ones",
|
||||
# we keep canonical product and delete other products in the group.
|
||||
#
|
||||
# IMPORTANT:
|
||||
# - inventories already point to SKUs, so safe
|
||||
# - SKUs already moved to canonical product
|
||||
# - modifier categories already moved
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Delete product_versions for old (non-canonical) products
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM product_versions pv
|
||||
USING tmp_product_canon m
|
||||
WHERE pv.product_id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Delete products for old (non-canonical) products
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM products p
|
||||
USING tmp_product_canon m
|
||||
WHERE p.id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@ -4,7 +4,7 @@ import uuid
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Uuid, text
|
||||
from sqlalchemy import Integer, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
||||
class Product:
|
||||
__tablename__ = "products"
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
versions: Mapped[list[ProductVersion]] = relationship(back_populates="product")
|
||||
skus: Mapped[list[StockKeepingUnit]] = relationship("StockKeepingUnit", back_populates="product")
|
||||
modifier_categories: Mapped[list[ModifierCategory]] = relationship(
|
||||
@ -30,6 +31,7 @@ class Product:
|
||||
back_populates="products",
|
||||
)
|
||||
|
||||
def __init__(self, id_: uuid.UUID | None = None):
|
||||
def __init__(self, sort_order: int = 0, id_: uuid.UUID | None = None):
|
||||
self.sort_order = sort_order
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
|
||||
@ -8,7 +8,6 @@ from typing import TYPE_CHECKING
|
||||
from sqlalchemy import (
|
||||
Date,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
Text,
|
||||
Uuid,
|
||||
func,
|
||||
@ -37,7 +36,6 @@ class ProductVersion:
|
||||
ForeignKey("sale_categories.id"),
|
||||
nullable=False,
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
valid_from: Mapped[date | None] = mapped_column(Date(), nullable=True)
|
||||
valid_till: Mapped[date | None] = mapped_column(Date(), nullable=True)
|
||||
@ -65,7 +63,6 @@ class ProductVersion:
|
||||
sale_category_id: uuid.UUID | None = None,
|
||||
valid_from: date | None = None,
|
||||
valid_till: date | None = None,
|
||||
sort_order: int = 0,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.product_id = product_id
|
||||
@ -75,6 +72,5 @@ class ProductVersion:
|
||||
self.sale_category_id = sale_category_id
|
||||
self.valid_from = valid_from
|
||||
self.valid_till = valid_till
|
||||
self.sort_order = sort_order
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
|
||||
@ -6,7 +6,7 @@ from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Date, ForeignKey, Integer, Numeric, Unicode, Uuid, func, text
|
||||
from sqlalchemy import Boolean, Date, ForeignKey, Numeric, Unicode, Uuid, func, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@ -34,7 +34,6 @@ class SkuVersion:
|
||||
cost_price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
sale_price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
has_happy_hour: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
menu_category_id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid,
|
||||
ForeignKey("menu_categories.id"),
|
||||
@ -66,7 +65,6 @@ class SkuVersion:
|
||||
cost_price: Decimal = Decimal("0.0"),
|
||||
sale_price: Decimal = Decimal("0.0"),
|
||||
has_happy_hour: bool = False,
|
||||
sort_order: int = 0,
|
||||
menu_category_id: uuid.UUID | None = None,
|
||||
sku_id: uuid.UUID | None = None,
|
||||
sku: StockKeepingUnit | None = None,
|
||||
@ -82,7 +80,6 @@ class SkuVersion:
|
||||
self.cost_price = cost_price
|
||||
self.sale_price = sale_price
|
||||
self.has_happy_hour = has_happy_hour
|
||||
self.sort_order = sort_order
|
||||
if menu_category_id is not None:
|
||||
self.menu_category_id = menu_category_id
|
||||
if id_ is not None:
|
||||
|
||||
@ -4,7 +4,7 @@ import uuid
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Uuid, text
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
@ -25,6 +25,7 @@ class StockKeepingUnit:
|
||||
)
|
||||
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False)
|
||||
is_not_available: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
product: Mapped[Product] = relationship("Product", back_populates="skus")
|
||||
inventories: Mapped[list[Inventory]] = relationship(back_populates="sku")
|
||||
@ -33,11 +34,13 @@ class StockKeepingUnit:
|
||||
def __init__(
|
||||
self,
|
||||
is_not_available: bool = False,
|
||||
sort_order: int = 0,
|
||||
product_id: uuid.UUID | None = None,
|
||||
product: Product | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
) -> None:
|
||||
self.is_not_available = is_not_available
|
||||
self.sort_order = sort_order
|
||||
if product_id is not None:
|
||||
self.product_id = product_id
|
||||
if id_ is not None:
|
||||
|
||||
@ -83,7 +83,7 @@ def save(
|
||||
) -> None:
|
||||
try:
|
||||
with SessionFuture() as db:
|
||||
product = Product()
|
||||
product = Product(sort_order=data.sort_order)
|
||||
db.add(product)
|
||||
db.flush() # so product.id is available
|
||||
version = ProductVersion(
|
||||
@ -91,7 +91,6 @@ def save(
|
||||
name=data.name,
|
||||
fraction_units=data.fraction_units,
|
||||
sale_category_id=data.sale_category.id_,
|
||||
sort_order=data.sort_order,
|
||||
valid_from=date_,
|
||||
valid_till=None,
|
||||
)
|
||||
@ -216,13 +215,13 @@ def update_route(
|
||||
or version.fraction_units != data.fraction_units
|
||||
or version.sale_category_id != data.sale_category.id_
|
||||
)
|
||||
version.product.sort_order = data.sort_order
|
||||
if pv_changed:
|
||||
if version.valid_from == date_:
|
||||
# Safe to mutate current version
|
||||
version.name = data.name
|
||||
version.fraction_units = data.fraction_units
|
||||
version.sale_category_id = data.sale_category.id_
|
||||
version.sort_order = data.sort_order
|
||||
else:
|
||||
# Close current and create a new version effective today
|
||||
version.valid_till = date_ - timedelta(days=1)
|
||||
@ -231,7 +230,6 @@ def update_route(
|
||||
name=data.name,
|
||||
fraction_units=data.fraction_units,
|
||||
sale_category_id=data.sale_category.id_,
|
||||
sort_order=data.sort_order,
|
||||
valid_from=date_,
|
||||
valid_till=None,
|
||||
)
|
||||
@ -251,7 +249,7 @@ def update_route(
|
||||
contains_eager(SkuVersion.menu_category),
|
||||
)
|
||||
.where(StockKeepingUnit.product_id == id_)
|
||||
.order_by(SkuVersion.sort_order, SkuVersion.units)
|
||||
.order_by(StockKeepingUnit.sort_order, SkuVersion.units)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
@ -274,6 +272,8 @@ def update_route(
|
||||
or new_data_sku.menu_category.id_ != sku.menu_category_id
|
||||
)
|
||||
print(f"SKU Changed: {sku_changed} for {sku.id}")
|
||||
sku.sku.sort_order = new_data_sku.sort_order
|
||||
sku.sku.is_not_available = new_data_sku.is_not_available
|
||||
if sku_changed:
|
||||
if sku.valid_from == date_:
|
||||
# Safe to mutate current version
|
||||
@ -295,7 +295,6 @@ def update_route(
|
||||
cost_price=round(new_data_sku.cost_price, 2),
|
||||
sale_price=round(new_data_sku.sale_price, 2),
|
||||
has_happy_hour=new_data_sku.has_happy_hour,
|
||||
sort_order=sku.sort_order,
|
||||
menu_category_id=new_data_sku.menu_category.id_,
|
||||
)
|
||||
db.add(new_sku)
|
||||
@ -431,9 +430,9 @@ def product_list(date_: date, db: Session) -> list[schemas.Product]:
|
||||
.order_by(
|
||||
MenuCategory.sort_order,
|
||||
MenuCategory.name,
|
||||
ProductVersion.sort_order,
|
||||
Product.sort_order,
|
||||
ProductVersion.name,
|
||||
SkuVersion.sort_order,
|
||||
StockKeepingUnit.sort_order,
|
||||
SkuVersion.units,
|
||||
)
|
||||
.options(
|
||||
@ -507,10 +506,16 @@ def show_term(
|
||||
if sc is not None:
|
||||
query = query.join(SkuVersion.menu_category).join(ProductVersion.sale_category).join(SaleCategory.tax)
|
||||
if mc is not None:
|
||||
query = query.where(SkuVersion.menu_category_id == mc).order_by(ProductVersion.sort_order, ProductVersion.name)
|
||||
query = query.where(SkuVersion.menu_category_id == mc).order_by(
|
||||
Product.sort_order, ProductVersion.name, StockKeepingUnit.sort_order, SkuVersion.units
|
||||
)
|
||||
if sc is not None:
|
||||
query = query.where(ProductVersion.sale_category_id == sc).order_by(
|
||||
MenuCategory.sort_order, ProductVersion.sort_order, ProductVersion.name
|
||||
MenuCategory.sort_order,
|
||||
Product.sort_order,
|
||||
ProductVersion.name,
|
||||
StockKeepingUnit.sort_order,
|
||||
SkuVersion.units,
|
||||
)
|
||||
|
||||
if mc is not None:
|
||||
@ -605,7 +610,7 @@ def show_id(
|
||||
.join(SkuVersion.menu_category)
|
||||
.where(pv_active)
|
||||
.order_by(
|
||||
SkuVersion.sort_order,
|
||||
StockKeepingUnit.sort_order,
|
||||
SkuVersion.units,
|
||||
)
|
||||
.options(
|
||||
@ -639,7 +644,7 @@ def query_product_info(item: SkuVersion, happy_hour: bool) -> ProductQuery:
|
||||
price=item.sale_price,
|
||||
has_happy_hour=happy_hour,
|
||||
is_not_available=item.sku.is_not_available,
|
||||
sort_order=item.sort_order,
|
||||
sort_order=item.sku.sort_order,
|
||||
)
|
||||
|
||||
|
||||
@ -666,12 +671,12 @@ def product_info(version: ProductVersion) -> schemas.Product:
|
||||
menu_category=MenuCategoryLink(
|
||||
id_=sku_version.menu_category_id, name=sku_version.menu_category.name, products=[]
|
||||
),
|
||||
sort_order=sku_version.sort_order,
|
||||
sort_order=sku_version.sku.sort_order,
|
||||
)
|
||||
for sku in version.product.skus
|
||||
for sku_version in sku.versions
|
||||
],
|
||||
sort_order=version.sort_order,
|
||||
sort_order=version.product.sort_order,
|
||||
)
|
||||
|
||||
|
||||
@ -683,23 +688,3 @@ def product_blank() -> schemas.ProductBlank:
|
||||
sort_order=0,
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
|
||||
# def product_info(item: ProductVersion) -> schemas.Product:
|
||||
# return schemas.Product(
|
||||
# id_=item.product_id,
|
||||
# units=item.units,
|
||||
|
||||
# sale_category=schemas.SaleCategoryLink(
|
||||
# id_=item.sale_category_id,
|
||||
# name=item.sale_category.name,
|
||||
# ),
|
||||
# price=item.price,
|
||||
# has_happy_hour=item.has_happy_hour,
|
||||
# is_not_available=item.is_not_available,
|
||||
# quantity=item.quantity,
|
||||
# is_active=True,
|
||||
# sort_order=item.sort_order,
|
||||
# valid_from=item.valid_from,
|
||||
# valid_till=item.valid_till,
|
||||
# )
|
||||
|
||||
Reference in New Issue
Block a user