Compare commits

..
13 Commits
Author SHA1 Message Date
tanshu d3741a63ed Version Bump v11.3.0 2023-08-12 08:39:43 +05:30
tanshu 1705d58dbc 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
2023-08-12 08:29:21 +05:30
tanshu 72843feaac Version Bump v11.2.1 2023-08-10 13:32:47 +05:30
tanshu e072e77663 Fix: Recipe add was not working and removed unused rate input in the recipe detail component 2023-08-10 13:32:35 +05:30
tanshu 9a3bd413d6 Version Bump v11.2.0 2023-08-08 13:07:00 +05:30
tanshu 2fa346e084 Fix: Fingerprints should have the right constraint
Fix: Client list was not working
2023-08-08 13:06:42 +05:30
tanshu ecd3e45632 Version Bump v11.1.9 2023-08-07 12:48:02 +05:30
tanshu 48ec2df10d Fix: Fingerprint was ignoring the time aspect which is obviously very important 2023-08-07 12:47:57 +05:30
tanshu bc61eeacd3 Version Bump v11.1.8 2023-08-07 10:58:49 +05:30
tanshu a051071a1b Fix: Fingerprint upload was broken 2023-08-07 10:58:41 +05:30
tanshu a0b939ccd7 Version Bump v11.1.7 2023-08-07 09:20:37 +05:30
tanshu 0fc8fac5aa Fix: docker should upload to gondor now not beacon 2023-08-07 09:20:21 +05:30
tanshu 220c15b3fa Fix: Pydantic v2 is sending decimals as strings and fucking things up 2023-08-07 09:19:48 +05:30
30 changed files with 420 additions and 121 deletions
@@ -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 ###
@@ -0,0 +1,23 @@
"""Fingerprint Index
Revision ID: 48af31eb6f3f
Revises: 12262aadbc08
Create Date: 2023-08-07 13:01:05.401492
"""
from alembic import op
# revision identifiers, used by Alembic.
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"])
def downgrade():
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"
@@ -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("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.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_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(
@@ -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 ###
+1 -1
View File
@@ -1 +1 @@
__version__ = "11.1.6"
__version__ = "11.3.0"
+1
View File
@@ -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
+2 -1
View File
@@ -3,7 +3,7 @@ import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, Uuid
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True)
class Fingerprint:
__tablename__ = "fingerprints"
__table_args__ = (UniqueConstraint("date", "employee_id"),)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
employee_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("employees.id"), nullable=False)
+51
View 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_
+27
View File
@@ -40,3 +40,30 @@ class Recipe:
tags: Mapped[list["Tag"]] = relationship(
"Tag", secondary=RecipeTag.__table__, order_by="Tag.name", back_populates="recipes"
)
def __init__(
self,
date_: date,
source: str,
instructions: str,
garnishing: str,
plating: str,
notes: str,
recipe_yield: Decimal,
sku_id: uuid.UUID | None = None,
sku: "StockKeepingUnit" | None = None,
id_: uuid.UUID | None = None,
):
self.date_ = date_
self.source = source
self.instructions = instructions
self.garnishing = garnishing
self.plating = plating
self.notes = notes
self.recipe_yield = recipe_yield
if sku_id is not None:
self.sku_id = sku_id
if sku is not None:
self.sku = sku
if id_ is not None:
self.id = id_
+23
View File
@@ -29,3 +29,26 @@ class RecipeItem:
recipe: Mapped["Recipe"] = relationship("Recipe", back_populates="items")
product: Mapped["Product"] = relationship("Product")
def __init__(
self,
quantity: Decimal,
description: str = "",
recipe_id: uuid.UUID | None = None,
product_id: uuid.UUID | None = None,
recipe: "Recipe" | None = None,
product: "Product" | None = None,
id_: uuid.UUID | None = None,
):
self.quantity = quantity
self.description = description
if recipe_id is not None:
self.recipe_id = recipe_id
if product_id is not None:
self.product_id = product_id
if recipe is not None:
self.recipe = recipe
if product is not None:
self.product = product
if id_ is not None:
self.id = id_
+131
View 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)
+5 -14
View File
@@ -8,7 +8,7 @@ from io import StringIO
import brewman.schemas.fingerprint as schemas
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlalchemy import bindparam, select
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
@@ -36,22 +36,13 @@ def upload_prints(
for id_, code in db.execute(select(Employee.id, Employee.code)).all():
employees[code] = id_
file_data = read_file(fingerprints)
prints = [d for d in fp(file_data, employees) if start <= d.date_.date() <= finish]
prints = [d.model_dump() for d in fp(file_data, employees) if start <= d.date_.date() <= finish]
for p in prints:
p["id"] = p.pop("id_")
paged_data = [prints[i : i + 100] for i in range(0, len(prints), 100)]
for i, page in enumerate(paged_data):
print(f"Processing page {i} of {len(paged_data)}")
db.execute(
pg_insert(Fingerprint)
.values(
{
"id": bindparam("id"),
"employee_id": bindparam("employee_id"),
"date": bindparam("date"),
}
)
.on_conflict_do_nothing(),
[p.dict() for p in page],
)
db.execute(pg_insert(Fingerprint).on_conflict_do_nothing(), page)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
+53 -28
View File
@@ -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
@@ -58,6 +60,7 @@ def save(
instructions=data.instructions,
garnishing=data.garnishing,
plating=data.plating,
notes=data.notes,
sku=recipe_sku,
recipe_yield=round(data.recipe_yield, 2),
)
@@ -71,7 +74,8 @@ def save(
r_item.recipe_id = recipe.id
db.add(r_item)
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:
@@ -128,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:
@@ -138,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)
@@ -167,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(
@@ -266,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)
@@ -280,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))
e.seek(0)
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)
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)
@@ -299,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
@@ -319,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=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()
wb.save(virtual_workbook)
virtual_workbook = BytesIO()
wb.save(virtual_workbook)
return virtual_workbook
+5 -3
View File
@@ -44,11 +44,13 @@ class ClientList(Client):
@field_validator("last_date", mode="before")
@classmethod
def parse_last_date(cls, value: datetime | str) -> datetime | None:
def parse_last_date(cls, value: datetime | str | None) -> datetime | None:
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("last_date")
def serialize_last_date(self, value: datetime, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y %H:%M")
def serialize_last_date(self, value: datetime | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
+4 -4
View File
@@ -20,11 +20,11 @@ class Fingerprint(BaseModel):
@field_validator("date_", mode="before")
@classmethod
def parse_date(cls, value: date | str) -> date:
if isinstance(value, date):
def parse_date(cls, value: datetime | str) -> datetime:
if isinstance(value, datetime):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
return datetime.strptime(value, "%d-%b-%Y %H:%M")
@field_serializer("date_")
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y")
return value.strftime("%d-%b-%Y %H:%M")
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "brewman"
version = "11.1.6"
version = "11.3.0"
description = "Accounting plus inventory management for a restaurant."
authors = ["tanshu <git@tanshu.com>"]
+1 -1
View File
@@ -32,5 +32,5 @@ else
echo "No version bump"
fi
cd "$parent_path/docker" || exit
docker save brewman:latest | bzip2 | pv | ssh beacon 'bunzip2 | sudo docker load'
docker save brewman:latest | bzip2 | pv | ssh gondor 'bunzip2 | sudo docker load'
ansible-playbook --inventory hosts playbook.yml
-2
View File
@@ -1,7 +1,5 @@
---
name: acc
# host_directory: "brewman-{{ name }}"
# db_name: "brewman_{{ name }}"
http_host: "acc.hopsngrains.com"
http_conf: "acc.hopsngrains.com.conf"
-2
View File
@@ -1,7 +1,5 @@
---
name: exp
# host_directory: "brewman-{{ name }}"
# db_name: "brewman_{{ name }}"
http_host: "exp.tanshu.com"
http_conf: "exp.tanshu.com.conf"
-2
View File
@@ -1,7 +1,5 @@
---
name: hinchco
# host_directory: "brewman-{{ name }}"
# db_name: "brewman_{{ name }}"
http_host: "acc.hinchco.in"
http_conf: "acc.hinchco.in.conf"
-2
View File
@@ -1,7 +1,5 @@
---
name: hops
# host_directory: "brewman-{{ name }}"
# db_name: "brewman_{{ name }}"
http_host: "hops.hopsngrains.com"
http_conf: "hops.hopsngrains.com.conf"
-2
View File
@@ -1,7 +1,5 @@
---
name: mhl
# host_directory: "brewman-{{ name }}"
# db_name: "brewman_{{ name }}"
http_host: "mhl.hopsngrains.com"
http_conf: "mhl.hopsngrains.com.conf"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "overlord",
"version": "11.1.6",
"version": "11.3.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
+4 -4
View File
@@ -193,10 +193,10 @@ export class IssueComponent implements OnInit, AfterViewInit, OnDestroy {
this.voucher.inventories.push(
new Inventory({
quantity,
rate: this.batch.rate,
tax: this.batch.tax,
discount: this.batch.discount,
amount: quantity * this.batch.rate * (1 + this.batch.tax) * (1 - this.batch.discount),
rate: +this.batch.rate,
tax: +this.batch.tax,
discount: +this.batch.discount,
amount: quantity * +this.batch.rate * (1 + +this.batch.tax) * (1 - +this.batch.discount),
batch: this.batch,
}),
);
@@ -24,7 +24,7 @@
</mat-form-field>
</div>
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-3/5 mr-5">
<mat-form-field class="flex-auto basis-4/5 mr-5">
<mat-label>Product</mat-label>
<input
type="text"
@@ -43,7 +43,7 @@
<mat-option *ngFor="let product of products | async" [value]="product">{{ product.name }}</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="flex-auto basis-1/10 mr-5">
<mat-form-field class="flex-auto basis-1/5">
<mat-label>Yield</mat-label>
<input type="text" matInput formControlName="recipeYield" autocomplete="off" />
</mat-form-field>
@@ -74,17 +74,12 @@
<mat-label>Quantity</mat-label>
<input type="text" matInput formControlName="quantity" autocomplete="off" />
</mat-form-field>
<mat-form-field class="flex-auto basis-1/5 mr-5">
<mat-form-field class="flex-auto basis-[30%] mr-5">
<mat-label>Description</mat-label>
<input type="text" matInput formControlName="description" autocomplete="off" />
</mat-form-field>
<mat-form-field class="flex-auto basis-1/10 mr-5">
<mat-label>Rate</mat-label>
<input type="text" matInput formControlName="rate" autocomplete="off" />
<span matTextPrefix>&nbsp;</span>
</mat-form-field>
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-1/10">Add</button>
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto basis-[10%]">Add</button>
</div>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
<!-- Ingredient Column -->
@@ -39,7 +39,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: FormControl<string | null>;
quantity: FormControl<string>;
description: FormControl<string>;
rate: FormControl<string>;
}>;
instructions: FormControl<string>;
garnishing: FormControl<string>;
@@ -75,7 +74,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: new FormControl(''),
quantity: new FormControl('', { nonNullable: true }),
description: new FormControl('', { nonNullable: true }),
rate: new FormControl('', { nonNullable: true }),
}),
instructions: new FormControl('', { nonNullable: true }),
garnishing: new FormControl('', { nonNullable: true }),
@@ -118,7 +116,6 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredient: null,
quantity: '',
description: '',
rate: '',
},
instructions: item.instructions,
garnishing: item.garnishing,
@@ -160,8 +157,7 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
return;
}
const quantity = this.math.parseAmount(formValue.quantity, 2);
const rate = this.math.parseAmount(formValue.rate, 2);
if (this.ingredient === null || quantity <= 0 || rate <= 0) {
if (this.ingredient === null || quantity <= 0) {
return;
}
const oldFiltered = this.item.items.filter((x) => x.product.id === (this.ingredient as ProductSku).id);
@@ -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`;
}
}
@@ -2,5 +2,5 @@ export const environment = {
production: true,
// eslint-disable-next-line @typescript-eslint/naming-convention
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.1.6',
version: '11.3.0',
};
+1 -1
View File
@@ -6,7 +6,7 @@ export const environment = {
production: false,
// eslint-disable-next-line @typescript-eslint/naming-convention
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '11.1.6',
version: '11.3.0',
};
/*