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
@ -5,41 +5,46 @@ Revises: a1372ed99c45
|
||||
Create Date: 2023-04-14 07:50:22.110724
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '12262aadbc08'
|
||||
down_revision = 'a1372ed99c45'
|
||||
revision = "12262aadbc08"
|
||||
down_revision = "a1372ed99c45"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('recipe_templates',
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('name', sa.Unicode(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.Column('text', sa.Unicode(), nullable=False),
|
||||
sa.Column('selected', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_recipe_templates')),
|
||||
sa.UniqueConstraint('name', name=op.f('uq_recipe_templates_name'))
|
||||
op.create_table(
|
||||
"recipe_templates",
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sa.Unicode(), nullable=False),
|
||||
sa.Column("date", sa.Date(), nullable=False),
|
||||
sa.Column("text", sa.Unicode(), nullable=False),
|
||||
sa.Column("selected", sa.Boolean(), nullable=False),
|
||||
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.alter_column('recipes', 'notes',
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
type_=sa.Text(),
|
||||
existing_nullable=False)
|
||||
op.create_index(
|
||||
"only_one_selected_template",
|
||||
"recipe_templates",
|
||||
["selected"],
|
||||
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 ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('recipes', 'notes',
|
||||
existing_type=sa.Text(),
|
||||
type_=sa.VARCHAR(length=255),
|
||||
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.alter_column("recipes", "notes", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), 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")
|
||||
# ### end Alembic commands ###
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""FP
|
||||
"""Fingerprint Index
|
||||
|
||||
Revision ID: 48af31eb6f3f
|
||||
Revises: 12262aadbc08
|
||||
@ -6,19 +6,18 @@ Create Date: 2023-08-07 13:01:05.401492
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '48af31eb6f3f'
|
||||
down_revision = '12262aadbc08'
|
||||
revision = "48af31eb6f3f"
|
||||
down_revision = "12262aadbc08"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
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():
|
||||
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
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a1372ed99c45"
|
||||
@ -63,14 +64,14 @@ def upgrade():
|
||||
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.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.drop_constraint("fk_recipes_period_id_periods", "recipes", type_="foreignkey")
|
||||
op.drop_column("recipes", "period_id")
|
||||
op.drop_column('recipes', 'sale_price')
|
||||
op.drop_column('recipes', 'cost_price')
|
||||
op.add_column('recipe_items', sa.Column('description', sa.Text(), nullable=False, server_default=""))
|
||||
op.drop_column('recipe_items', 'price')
|
||||
op.drop_column("recipes", "sale_price")
|
||||
op.drop_column("recipes", "cost_price")
|
||||
op.add_column("recipe_items", sa.Column("description", sa.Text(), nullable=False, server_default=""))
|
||||
op.drop_column("recipe_items", "price")
|
||||
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.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.period import Period # 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_group import ProductGroup # 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_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.responses import FileResponse, StreamingResponse
|
||||
from openpyxl import Workbook
|
||||
@ -72,8 +74,8 @@ def save(
|
||||
r_item.recipe_id = recipe.id
|
||||
db.add(r_item)
|
||||
|
||||
# TODO: Check recursion
|
||||
# check_recursion(set([recipe_sku.product_id]), set(), recipe, db)
|
||||
db.flush()
|
||||
check_recursion(recipe_sku.product_id, set(), db)
|
||||
db.commit()
|
||||
return recipe_info(recipe)
|
||||
except SQLAlchemyError as e:
|
||||
@ -130,7 +132,8 @@ async def update_route(
|
||||
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()
|
||||
return recipe_info(recipe)
|
||||
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:
|
||||
sq = (
|
||||
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):
|
||||
def check_recursion(product: uuid.UUID, visited: set[uuid.UUID], db: Session) -> None:
|
||||
if product in visited:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
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)
|
||||
@ -169,7 +169,6 @@ def delete_route(
|
||||
user: UserToken = Security(get_user, scopes=["recipes"]),
|
||||
) -> None:
|
||||
with SessionFuture() as db:
|
||||
recipe: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
|
||||
recipe_ids: Sequence[uuid.UUID] = (
|
||||
db.execute(
|
||||
select(func.distinct(RecipeItem.recipe_id)).where(
|
||||
@ -268,7 +267,22 @@ def show_pdf(
|
||||
@router.get("/xlsx", response_class=StreamingResponse)
|
||||
def get_report(
|
||||
p: uuid.UUID | None = None,
|
||||
t: uuid.UUID | None = None,
|
||||
) -> 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:
|
||||
q = (
|
||||
select(Recipe)
|
||||
@ -282,17 +296,25 @@ def get_report(
|
||||
)
|
||||
if p is not None:
|
||||
q = q.where(Recipe.sku, StockKeepingUnit.product, Product.product_group_id == p)
|
||||
list_: Sequence[Recipe] = db.execute(q).unique().scalars().all()
|
||||
e = excel(sorted(list_, key=lambda r: r.sku.product.name))
|
||||
list_ = db.execute(q).unique().scalars().all()
|
||||
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.name))
|
||||
e.seek(0)
|
||||
|
||||
headers = {"Content-Disposition": "attachment; filename = recipe.xlsx"}
|
||||
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.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])
|
||||
for pg in pgs:
|
||||
wb.create_sheet(pg)
|
||||
@ -301,6 +323,7 @@ def excel(recipes: list[Recipe]) -> BytesIO:
|
||||
for recipe in recipes:
|
||||
ws = wb[recipe.sku.product.product_group.name]
|
||||
row = rows[recipe.sku.product.product_group.name]
|
||||
print(row)
|
||||
ings = len(recipe.items)
|
||||
ing_from = row + 2
|
||||
ing_till = ing_from + ings - 1
|
||||
@ -321,7 +344,7 @@ def excel(recipes: list[Recipe]) -> BytesIO:
|
||||
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=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"
|
||||
rows[recipe.sku.product.product_group.name] = row + 1
|
||||
virtual_workbook = BytesIO()
|
||||
|
@ -2,7 +2,7 @@
|
||||
<mat-card-header>
|
||||
<mat-card-title-group>
|
||||
<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>
|
||||
</a>
|
||||
<a mat-button [routerLink]="['/recipes', 'new']">
|
||||
|
@ -31,6 +31,7 @@ export class RecipeListComponent implements OnInit {
|
||||
list: Recipe[] = [];
|
||||
data: BehaviorSubject<Recipe[]> = new BehaviorSubject<Recipe[]>([]);
|
||||
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. */
|
||||
displayedColumns = ['name', 'yield', 'date', 'source'];
|
||||
@ -44,13 +45,14 @@ export class RecipeListComponent implements OnInit {
|
||||
productGroup: new FormControl<ProductGroup | string | null>(null),
|
||||
});
|
||||
// Listen to Payment Account Change
|
||||
this.form.controls.period.valueChanges.subscribe((x) =>
|
||||
this.form.controls.period.valueChanges.subscribe((x) => {
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { p: x.id },
|
||||
replaceUrl: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
this.period = x;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@ -73,8 +75,4 @@ export class RecipeListComponent implements OnInit {
|
||||
filterProductGroup(val: string) {
|
||||
this.productGroupFilter.next(val || '');
|
||||
}
|
||||
|
||||
excelLink() {
|
||||
return `/api/recipes/xlsx`;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user