Fix: Recipe now checks for recursion (hopefully)
Feature: Recipe prices are now calculated based on periods and saved Feature: The recipe export excel now has prices
This commit is contained in:
parent
72843feaac
commit
1705d58dbc
brewman
alembic/versions
12262aadbc08_recipe_templates.py48af31eb6f3f_fp.pya1372ed99c45_recipe_upgrade.pyba0fff092981_price.py
brewman
overlord/src/app/recipe/recipe-list
@ -5,41 +5,46 @@ Revises: a1372ed99c45
|
|||||||
Create Date: 2023-04-14 07:50:22.110724
|
Create Date: 2023-04-14 07:50:22.110724
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '12262aadbc08'
|
revision = "12262aadbc08"
|
||||||
down_revision = 'a1372ed99c45'
|
down_revision = "a1372ed99c45"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('recipe_templates',
|
op.create_table(
|
||||||
sa.Column('id', sa.Uuid(), nullable=False),
|
"recipe_templates",
|
||||||
sa.Column('name', sa.Unicode(), nullable=False),
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
sa.Column('date', sa.Date(), nullable=False),
|
sa.Column("name", sa.Unicode(), nullable=False),
|
||||||
sa.Column('text', sa.Unicode(), nullable=False),
|
sa.Column("date", sa.Date(), nullable=False),
|
||||||
sa.Column('selected', sa.Boolean(), nullable=False),
|
sa.Column("text", sa.Unicode(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_recipe_templates')),
|
sa.Column("selected", sa.Boolean(), nullable=False),
|
||||||
sa.UniqueConstraint('name', name=op.f('uq_recipe_templates_name'))
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_recipe_templates")),
|
||||||
|
sa.UniqueConstraint("name", name=op.f("uq_recipe_templates_name")),
|
||||||
)
|
)
|
||||||
op.create_index('only_one_selected_template', 'recipe_templates', ['selected'], unique=True, postgresql_where=sa.text('selected = true'))
|
op.create_index(
|
||||||
op.alter_column('recipes', 'notes',
|
"only_one_selected_template",
|
||||||
existing_type=sa.VARCHAR(length=255),
|
"recipe_templates",
|
||||||
type_=sa.Text(),
|
["selected"],
|
||||||
existing_nullable=False)
|
unique=True,
|
||||||
|
postgresql_where=sa.text("selected = true"),
|
||||||
|
)
|
||||||
|
op.alter_column("recipes", "notes", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.alter_column('recipes', 'notes',
|
op.alter_column("recipes", "notes", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||||
existing_type=sa.Text(),
|
op.drop_index(
|
||||||
type_=sa.VARCHAR(length=255),
|
"only_one_selected_template", table_name="recipe_templates", postgresql_where=sa.text("selected = true")
|
||||||
existing_nullable=False)
|
)
|
||||||
op.drop_index('only_one_selected_template', table_name='recipe_templates', postgresql_where=sa.text('selected = true'))
|
op.drop_table("recipe_templates")
|
||||||
op.drop_table('recipe_templates')
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""FP
|
"""Fingerprint Index
|
||||||
|
|
||||||
Revision ID: 48af31eb6f3f
|
Revision ID: 48af31eb6f3f
|
||||||
Revises: 12262aadbc08
|
Revises: 12262aadbc08
|
||||||
@ -6,19 +6,18 @@ Create Date: 2023-08-07 13:01:05.401492
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '48af31eb6f3f'
|
revision = "48af31eb6f3f"
|
||||||
down_revision = '12262aadbc08'
|
down_revision = "12262aadbc08"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
op.create_unique_constraint(op.f('uq_fingerprints_date'), 'fingerprints', ['date', 'employee_id'])
|
op.create_unique_constraint(op.f("uq_fingerprints_date"), "fingerprints", ["date", "employee_id"])
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
op.drop_constraint(op.f('uq_fingerprints_date'), 'fingerprints', type_='unique')
|
op.drop_constraint(op.f("uq_fingerprints_date"), "fingerprints", type_="unique")
|
||||||
|
@ -8,8 +8,9 @@ Create Date: 2023-03-31 05:03:40.408240
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = "a1372ed99c45"
|
revision = "a1372ed99c45"
|
||||||
@ -62,15 +63,15 @@ def upgrade():
|
|||||||
op.add_column("recipes", sa.Column("garnishing", sa.Text(), nullable=False, server_default=""))
|
op.add_column("recipes", sa.Column("garnishing", sa.Text(), nullable=False, server_default=""))
|
||||||
op.add_column("recipes", sa.Column("plating", sa.Text(), nullable=False, server_default=""))
|
op.add_column("recipes", sa.Column("plating", sa.Text(), nullable=False, server_default=""))
|
||||||
op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique")
|
op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique")
|
||||||
op.alter_column("recipes", "notes", existing_type=sa.VARCHAR(length=255),type=sa.Text(), nullable=False)
|
op.alter_column("recipes", "notes", existing_type=sa.VARCHAR(length=255), type=sa.Text(), nullable=False)
|
||||||
op.create_unique_constraint(op.f('uq_recipes_sku_id'), 'recipes', ['sku_id', 'date'])
|
op.create_unique_constraint(op.f("uq_recipes_sku_id"), "recipes", ["sku_id", "date"])
|
||||||
op.create_index(op.f("ix_recipes_date"), "recipes", ["date"], unique=False)
|
op.create_index(op.f("ix_recipes_date"), "recipes", ["date"], unique=False)
|
||||||
op.drop_constraint("fk_recipes_period_id_periods", "recipes", type_="foreignkey")
|
op.drop_constraint("fk_recipes_period_id_periods", "recipes", type_="foreignkey")
|
||||||
op.drop_column("recipes", "period_id")
|
op.drop_column("recipes", "period_id")
|
||||||
op.drop_column('recipes', 'sale_price')
|
op.drop_column("recipes", "sale_price")
|
||||||
op.drop_column('recipes', 'cost_price')
|
op.drop_column("recipes", "cost_price")
|
||||||
op.add_column('recipe_items', sa.Column('description', sa.Text(), nullable=False, server_default=""))
|
op.add_column("recipe_items", sa.Column("description", sa.Text(), nullable=False, server_default=""))
|
||||||
op.drop_column('recipe_items', 'price')
|
op.drop_column("recipe_items", "price")
|
||||||
op.alter_column("role_permissions", "permission_id", existing_type=sa.UUID(), nullable=False)
|
op.alter_column("role_permissions", "permission_id", existing_type=sa.UUID(), nullable=False)
|
||||||
op.alter_column("role_permissions", "role_id", existing_type=sa.UUID(), nullable=False)
|
op.alter_column("role_permissions", "role_id", existing_type=sa.UUID(), nullable=False)
|
||||||
op.create_unique_constraint(
|
op.create_unique_constraint(
|
||||||
|
39
brewman/alembic/versions/ba0fff092981_price.py
Normal file
39
brewman/alembic/versions/ba0fff092981_price.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""price
|
||||||
|
|
||||||
|
Revision ID: ba0fff092981
|
||||||
|
Revises: 48af31eb6f3f
|
||||||
|
Create Date: 2023-08-11 18:12:51.293741
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "ba0fff092981"
|
||||||
|
down_revision = "48af31eb6f3f"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"prices",
|
||||||
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column("period_id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column("product_id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column("price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["period_id"], ["periods.id"], name=op.f("fk_prices_period_id_periods")),
|
||||||
|
sa.ForeignKeyConstraint(["product_id"], ["products.id"], name=op.f("fk_prices_product_id_products")),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_prices")),
|
||||||
|
sa.UniqueConstraint("period_id", "product_id", name=op.f("uq_prices_period_id")),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table("prices")
|
||||||
|
# ### end Alembic commands ###
|
@ -19,6 +19,7 @@ from ..models.journal import Journal # noqa: F401
|
|||||||
from ..models.login_history import LoginHistory # noqa: F401
|
from ..models.login_history import LoginHistory # noqa: F401
|
||||||
from ..models.period import Period # noqa: F401
|
from ..models.period import Period # noqa: F401
|
||||||
from ..models.permission import Permission # noqa: F401
|
from ..models.permission import Permission # noqa: F401
|
||||||
|
from ..models.price import Price # noqa: F401
|
||||||
from ..models.product import Product # noqa: F401
|
from ..models.product import Product # noqa: F401
|
||||||
from ..models.product_group import ProductGroup # noqa: F401
|
from ..models.product_group import ProductGroup # noqa: F401
|
||||||
from ..models.rate_contract import RateContract # noqa: F401
|
from ..models.rate_contract import RateContract # noqa: F401
|
||||||
|
51
brewman/brewman/models/price.py
Normal file
51
brewman/brewman/models/price.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, Numeric, UniqueConstraint, Uuid
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from ..db.base_class import reg
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .period import Period
|
||||||
|
from .product import Product
|
||||||
|
|
||||||
|
|
||||||
|
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||||
|
class Price:
|
||||||
|
__tablename__ = "prices"
|
||||||
|
__table_args__ = (UniqueConstraint("period_id", "product_id"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
|
||||||
|
period_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("periods.id"), nullable=False)
|
||||||
|
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False)
|
||||||
|
price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||||
|
|
||||||
|
period: Mapped["Period"] = relationship("Period")
|
||||||
|
product: Mapped["Product"] = relationship("Product")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
price: Decimal,
|
||||||
|
period_id: uuid.UUID | None = None,
|
||||||
|
product_id: uuid.UUID | None = None,
|
||||||
|
period: "Period" | None = None,
|
||||||
|
product: "Product" | None = None,
|
||||||
|
id_: uuid.UUID | None = None,
|
||||||
|
):
|
||||||
|
self.price = price
|
||||||
|
if period_id is not None:
|
||||||
|
self.period_id = period_id
|
||||||
|
if product_id is not None:
|
||||||
|
self.product_id = product_id
|
||||||
|
if period is not None and (period.id is not None or period_id is None):
|
||||||
|
self.period = period
|
||||||
|
if product is not None and (product.id is not None or product_id is None):
|
||||||
|
self.product = product
|
||||||
|
if id_ is not None:
|
||||||
|
self.id = id_
|
131
brewman/brewman/routers/calculate_prices.py
Normal file
131
brewman/brewman/routers/calculate_prices.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from brewman.models.batch import Batch
|
||||||
|
from brewman.models.cost_centre import CostCentre
|
||||||
|
from brewman.models.inventory import Inventory
|
||||||
|
from brewman.models.journal import Journal
|
||||||
|
from brewman.models.period import Period
|
||||||
|
from brewman.models.price import Price
|
||||||
|
from brewman.models.voucher import Voucher
|
||||||
|
from brewman.models.voucher_type import VoucherType
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import distinct, func, select
|
||||||
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from ..models.recipe import Recipe
|
||||||
|
from ..models.recipe_item import RecipeItem
|
||||||
|
from ..models.stock_keeping_unit import StockKeepingUnit
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_prices(period_id: uuid.UUID, db: Session):
|
||||||
|
try:
|
||||||
|
item: Period = db.execute(select(Period).where(Period.id == period_id)).scalar_one()
|
||||||
|
recipes = set(
|
||||||
|
db.execute(select(distinct(StockKeepingUnit.product_id)).join(StockKeepingUnit.recipes)).scalars().all()
|
||||||
|
)
|
||||||
|
ingredients = set(db.execute(select(distinct(RecipeItem.product_id))).scalars().all())
|
||||||
|
issued_products = get_issue_prices(item, ingredients - recipes, db)
|
||||||
|
left = ingredients - recipes - issued_products.keys()
|
||||||
|
purchased_products = get_issue_prices(item, left, db)
|
||||||
|
left -= purchased_products.keys()
|
||||||
|
rest = get_rest(left, db)
|
||||||
|
prices = issued_products | purchased_products | rest
|
||||||
|
|
||||||
|
while len(recipes) > 0:
|
||||||
|
calculate_recipes(recipes, prices, db)
|
||||||
|
for pid, price in prices.items():
|
||||||
|
db.execute(
|
||||||
|
pg_insert(Price)
|
||||||
|
.values(product_id=pid, price=price, period_id=item.id)
|
||||||
|
.on_conflict_do_update(constraint="uq_prices_period_id", set_=dict(price=price))
|
||||||
|
)
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_issue_prices(period: Period, products: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
|
||||||
|
sum_quantity = func.sum(
|
||||||
|
Inventory.quantity * StockKeepingUnit.fraction * StockKeepingUnit.product_yield * Journal.debit
|
||||||
|
).label("quantity")
|
||||||
|
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
|
||||||
|
|
||||||
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
|
query: list[tuple[uuid.UUID, Decimal]] = db.execute(
|
||||||
|
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
|
||||||
|
.join(Inventory.batch)
|
||||||
|
.join(Batch.sku)
|
||||||
|
.join(Inventory.voucher)
|
||||||
|
.join(Voucher.journals)
|
||||||
|
.where(
|
||||||
|
Voucher.date_ >= period.valid_from,
|
||||||
|
Voucher.date_ <= period.valid_till,
|
||||||
|
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
|
||||||
|
Journal.cost_centre_id != CostCentre.cost_centre_purchase(),
|
||||||
|
StockKeepingUnit.product_id.in_(products),
|
||||||
|
)
|
||||||
|
.group_by(StockKeepingUnit.product_id, Journal.debit)
|
||||||
|
).all()
|
||||||
|
for id, amount in query:
|
||||||
|
d[id] = amount
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
|
||||||
|
sum_quantity = func.sum(
|
||||||
|
Inventory.quantity * StockKeepingUnit.fraction * StockKeepingUnit.product_yield * Journal.debit
|
||||||
|
).label("quantity")
|
||||||
|
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
|
||||||
|
|
||||||
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
|
query: list[tuple[uuid.UUID, Decimal]] = db.execute(
|
||||||
|
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
|
||||||
|
.join(Inventory.batch)
|
||||||
|
.join(Batch.sku)
|
||||||
|
.join(Inventory.voucher)
|
||||||
|
.join(Voucher.journals)
|
||||||
|
.where(
|
||||||
|
Voucher.date_ >= period.valid_from,
|
||||||
|
Voucher.date_ <= period.valid_till,
|
||||||
|
Voucher.voucher_type == VoucherType.PURCHASE,
|
||||||
|
StockKeepingUnit.product_id.in_(req),
|
||||||
|
)
|
||||||
|
.group_by(StockKeepingUnit.product_id, Journal.debit)
|
||||||
|
).all()
|
||||||
|
for id, amount in query:
|
||||||
|
d[id] = amount
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_rest(req: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
|
||||||
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
|
query = db.execute(
|
||||||
|
select(
|
||||||
|
StockKeepingUnit.product_id,
|
||||||
|
StockKeepingUnit.cost_price / (StockKeepingUnit.fraction * StockKeepingUnit.product_yield),
|
||||||
|
).where(StockKeepingUnit.product_id.in_(req))
|
||||||
|
).all()
|
||||||
|
for id, amount in query:
|
||||||
|
d[id] = amount
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_recipes(recipes: set[uuid.UUID], prices: dict[uuid.UUID, Decimal], db: Session) -> None:
|
||||||
|
sq = select(RecipeItem.recipe_id).where(RecipeItem.product_id.in_(recipes))
|
||||||
|
items = (
|
||||||
|
db.execute(
|
||||||
|
select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(recipes), Recipe.id.notin_(sq))
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for item in items:
|
||||||
|
cost = sum(i.quantity * prices[i.product_id] for i in item.items) / (item.recipe_yield * item.sku.fraction)
|
||||||
|
prices[item.sku.product_id] = cost
|
||||||
|
recipes.remove(item.sku.product_id)
|
@ -11,6 +11,8 @@ from typing import Sequence
|
|||||||
import brewman.schemas.recipe as schemas
|
import brewman.schemas.recipe as schemas
|
||||||
import brewman.schemas.recipe_item as rischemas
|
import brewman.schemas.recipe_item as rischemas
|
||||||
|
|
||||||
|
from brewman.models.price import Price
|
||||||
|
from brewman.routers.calculate_prices import calculate_prices
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
@ -72,8 +74,8 @@ def save(
|
|||||||
r_item.recipe_id = recipe.id
|
r_item.recipe_id = recipe.id
|
||||||
db.add(r_item)
|
db.add(r_item)
|
||||||
|
|
||||||
# TODO: Check recursion
|
db.flush()
|
||||||
# check_recursion(set([recipe_sku.product_id]), set(), recipe, db)
|
check_recursion(recipe_sku.product_id, set(), db)
|
||||||
db.commit()
|
db.commit()
|
||||||
return recipe_info(recipe)
|
return recipe_info(recipe)
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
@ -130,7 +132,8 @@ async def update_route(
|
|||||||
RecipeItem(product_id=product.id, quantity=quantity, description=d_item.description)
|
RecipeItem(product_id=product.id, quantity=quantity, description=d_item.description)
|
||||||
)
|
)
|
||||||
|
|
||||||
check_recursion(set([sku.product_id]), set(), db)
|
db.flush()
|
||||||
|
check_recursion(sku.product_id, set(), db)
|
||||||
db.commit()
|
db.commit()
|
||||||
return recipe_info(recipe)
|
return recipe_info(recipe)
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
@ -140,26 +143,23 @@ async def update_route(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_recursion(products: set[uuid.UUID], visited: set[uuid.UUID], db: Session) -> None:
|
def check_recursion(product: uuid.UUID, visited: set[uuid.UUID], db: Session) -> None:
|
||||||
sq = (
|
if product in visited:
|
||||||
select(func.distinct(RecipeItem.product_id))
|
|
||||||
.join(Recipe.items)
|
|
||||||
.join(Recipe.sku)
|
|
||||||
.where(StockKeepingUnit.product_id.in_(products))
|
|
||||||
)
|
|
||||||
ingredient_product_ids = (
|
|
||||||
db.execute(select(StockKeepingUnit.product_id).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(sq)))
|
|
||||||
.scalars()
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if len(ingredient_product_ids) == 0:
|
|
||||||
return
|
|
||||||
if (visited | products) & set(ingredient_product_ids):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail="Recipe recursion. Some ingredient recipe contains parent recipe.",
|
detail="Recipe recursion. Some ingredient recipe contains parent recipe.",
|
||||||
)
|
)
|
||||||
check_recursion(set(ingredient_product_ids), visited | products, db)
|
recipe: Recipe = (
|
||||||
|
db.execute(select(Recipe).join(Recipe.items).join(Recipe.sku).where(StockKeepingUnit.product_id == product))
|
||||||
|
.unique()
|
||||||
|
.scalar_one_or_none()
|
||||||
|
)
|
||||||
|
if recipe is None:
|
||||||
|
return
|
||||||
|
visited.add(product)
|
||||||
|
for i in recipe.items:
|
||||||
|
check_recursion(i.product_id, visited, db)
|
||||||
|
visited.remove(product)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id_}", response_model=None)
|
@router.delete("/{id_}", response_model=None)
|
||||||
@ -169,7 +169,6 @@ def delete_route(
|
|||||||
user: UserToken = Security(get_user, scopes=["recipes"]),
|
user: UserToken = Security(get_user, scopes=["recipes"]),
|
||||||
) -> None:
|
) -> None:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
recipe: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
|
|
||||||
recipe_ids: Sequence[uuid.UUID] = (
|
recipe_ids: Sequence[uuid.UUID] = (
|
||||||
db.execute(
|
db.execute(
|
||||||
select(func.distinct(RecipeItem.recipe_id)).where(
|
select(func.distinct(RecipeItem.recipe_id)).where(
|
||||||
@ -268,7 +267,22 @@ def show_pdf(
|
|||||||
@router.get("/xlsx", response_class=StreamingResponse)
|
@router.get("/xlsx", response_class=StreamingResponse)
|
||||||
def get_report(
|
def get_report(
|
||||||
p: uuid.UUID | None = None,
|
p: uuid.UUID | None = None,
|
||||||
|
t: uuid.UUID | None = None,
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
|
with SessionFuture() as db:
|
||||||
|
calculate_prices(t, db)
|
||||||
|
db.commit()
|
||||||
|
prices: list[tuple[str, str, Decimal]] = []
|
||||||
|
with SessionFuture() as db:
|
||||||
|
pq = (
|
||||||
|
db.execute(select(Price).where(Price.period_id == t).options(joinedload(Price.product, innerjoin=True)))
|
||||||
|
.unique()
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
prices = [(i.product.name, i.product.fraction_units, i.price) for i in pq]
|
||||||
|
|
||||||
|
list_: Sequence[Recipe] = []
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
q = (
|
q = (
|
||||||
select(Recipe)
|
select(Recipe)
|
||||||
@ -282,17 +296,25 @@ def get_report(
|
|||||||
)
|
)
|
||||||
if p is not None:
|
if p is not None:
|
||||||
q = q.where(Recipe.sku, StockKeepingUnit.product, Product.product_group_id == p)
|
q = q.where(Recipe.sku, StockKeepingUnit.product, Product.product_group_id == p)
|
||||||
list_: Sequence[Recipe] = db.execute(q).unique().scalars().all()
|
list_ = db.execute(q).unique().scalars().all()
|
||||||
e = excel(sorted(list_, key=lambda r: r.sku.product.name))
|
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.name))
|
||||||
e.seek(0)
|
e.seek(0)
|
||||||
|
|
||||||
headers = {"Content-Disposition": "attachment; filename = recipe.xlsx"}
|
headers = {"Content-Disposition": "attachment; filename = recipe.xlsx"}
|
||||||
return StreamingResponse(e, media_type="text/xlsx", headers=headers)
|
return StreamingResponse(e, media_type="text/xlsx", headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def excel(recipes: list[Recipe]) -> BytesIO:
|
def excel(prices: list[tuple[str, str, Decimal, Decimal, Decimal]], recipes: list[Recipe]) -> BytesIO:
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
wb.active.title = "Rate List"
|
wb.active.title = "Rate List"
|
||||||
|
wb.active.cell(row=1, column=1, value="Name")
|
||||||
|
wb.active.cell(row=1, column=2, value="Units")
|
||||||
|
wb.active.cell(row=1, column=3, value="Rate")
|
||||||
|
for i, p in enumerate(prices, start=2):
|
||||||
|
wb.active.cell(row=i, column=1, value=p[0])
|
||||||
|
wb.active.cell(row=i, column=2, value=p[1])
|
||||||
|
wb.active.cell(row=i, column=3, value=p[2])
|
||||||
|
|
||||||
pgs = set([x.sku.product.product_group.name for x in recipes])
|
pgs = set([x.sku.product.product_group.name for x in recipes])
|
||||||
for pg in pgs:
|
for pg in pgs:
|
||||||
wb.create_sheet(pg)
|
wb.create_sheet(pg)
|
||||||
@ -301,6 +323,7 @@ def excel(recipes: list[Recipe]) -> BytesIO:
|
|||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
ws = wb[recipe.sku.product.product_group.name]
|
ws = wb[recipe.sku.product.product_group.name]
|
||||||
row = rows[recipe.sku.product.product_group.name]
|
row = rows[recipe.sku.product.product_group.name]
|
||||||
|
print(row)
|
||||||
ings = len(recipe.items)
|
ings = len(recipe.items)
|
||||||
ing_from = row + 2
|
ing_from = row + 2
|
||||||
ing_till = ing_from + ings - 1
|
ing_till = ing_from + ings - 1
|
||||||
@ -321,11 +344,11 @@ def excel(recipes: list[Recipe]) -> BytesIO:
|
|||||||
ws.cell(row=row, column=1, value=item.product.name).style = "ing"
|
ws.cell(row=row, column=1, value=item.product.name).style = "ing"
|
||||||
ws.cell(row=row, column=2, value=item.product.fraction_units).style = "unit"
|
ws.cell(row=row, column=2, value=item.product.fraction_units).style = "unit"
|
||||||
ws.cell(row=row, column=3, value=item.quantity).style = "ing"
|
ws.cell(row=row, column=3, value=item.quantity).style = "ing"
|
||||||
ws.cell(row=row, column=4, value="=VLOOKUP(A:A,'Rate List'!A:G,7,0)").style = "ing"
|
ws.cell(row=row, column=4, value="=VLOOKUP(A:A,'Rate List'!A:C,3,0)").style = "ing"
|
||||||
ws.cell(row=row, column=5, value=f"=C{row}*D{row}").style = "ing"
|
ws.cell(row=row, column=5, value=f"=C{row}*D{row}").style = "ing"
|
||||||
rows[recipe.sku.product.product_group.name] = row + 1
|
rows[recipe.sku.product.product_group.name] = row + 1
|
||||||
virtual_workbook = BytesIO()
|
virtual_workbook = BytesIO()
|
||||||
wb.save(virtual_workbook)
|
wb.save(virtual_workbook)
|
||||||
return virtual_workbook
|
return virtual_workbook
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title-group>
|
<mat-card-title-group>
|
||||||
<mat-card-title>Recipes</mat-card-title>
|
<mat-card-title>Recipes</mat-card-title>
|
||||||
<a mat-icon-button href="{{ excelLink() }}">
|
<a mat-icon-button [href]="'/api/recipes/xlsx?t=' + period.id">
|
||||||
<mat-icon>save_alt</mat-icon>
|
<mat-icon>save_alt</mat-icon>
|
||||||
</a>
|
</a>
|
||||||
<a mat-button [routerLink]="['/recipes', 'new']">
|
<a mat-button [routerLink]="['/recipes', 'new']">
|
||||||
|
@ -31,6 +31,7 @@ export class RecipeListComponent implements OnInit {
|
|||||||
list: Recipe[] = [];
|
list: Recipe[] = [];
|
||||||
data: BehaviorSubject<Recipe[]> = new BehaviorSubject<Recipe[]>([]);
|
data: BehaviorSubject<Recipe[]> = new BehaviorSubject<Recipe[]>([]);
|
||||||
dataSource: RecipeListDatasource = new RecipeListDatasource(this.productGroupFilter, this.data);
|
dataSource: RecipeListDatasource = new RecipeListDatasource(this.productGroupFilter, this.data);
|
||||||
|
period: Period = new Period();
|
||||||
|
|
||||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||||
displayedColumns = ['name', 'yield', 'date', 'source'];
|
displayedColumns = ['name', 'yield', 'date', 'source'];
|
||||||
@ -44,13 +45,14 @@ export class RecipeListComponent implements OnInit {
|
|||||||
productGroup: new FormControl<ProductGroup | string | null>(null),
|
productGroup: new FormControl<ProductGroup | string | null>(null),
|
||||||
});
|
});
|
||||||
// Listen to Payment Account Change
|
// Listen to Payment Account Change
|
||||||
this.form.controls.period.valueChanges.subscribe((x) =>
|
this.form.controls.period.valueChanges.subscribe((x) => {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
queryParams: { p: x.id },
|
queryParams: { p: x.id },
|
||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
}),
|
});
|
||||||
);
|
this.period = x;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -73,8 +75,4 @@ export class RecipeListComponent implements OnInit {
|
|||||||
filterProductGroup(val: string) {
|
filterProductGroup(val: string) {
|
||||||
this.productGroupFilter.next(val || '');
|
this.productGroupFilter.next(val || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
excelLink() {
|
|
||||||
return `/api/recipes/xlsx`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user