Changed the build system. Now I am targeting multi arch. Added Product Version and SkuVersion

This commit is contained in:
tanshu committed 2025-06-25 06:22:44 +00:00
1 parent 0d2dea4eff
commit 05b395af00
48 files changed
+2379 -561

No files matched your search

+13 -13
View File
@@ -1,22 +1,22 @@
TITLE="Brewman"
TITLE=Hops n Grains
HOST=0.0.0.0
PORT=80
LOG_LEVEL=WARN
PORT=9998
LOG_LEVEL=WARNING
DEBUG=true
SQLALCHEMY_DATABASE_URI=
SQLALCHEMY_DATABASE_URI=postgresql+psycopg://postgres:123456@10.1.1.1:5432/hops
MODULE_NAME=brewman.main
PROJECT_NAME=brewman
POSTGRES_SERVER=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
POSTGRES_SERVER=localhost
POSTGRES_USER=postgres
POSTGRES_PASSWORD=123456
POSTGRES_DB=hops
# openssl rand -hex 32
SECRET_KEY=
PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAX5qCMOidCSToKo2oe6eNii+Jg9qKiQDz2+JD/r3gQAs=\n-----END PUBLIC KEY-----
PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIHlhlEP7acCnOWq6VUWJN/q2NU2Mwo7eEtMo1em6luy7\n-----END PRIVATE KEY-----
# openssl rand -hex 5
MIDDLEWARE_SECRET_KEY=
ALGORITHM=HS256
MIDDLEWARE_SECRET_KEY=c982367648
ALGORITHM=EdDSA
JWT_TOKEN_EXPIRE_MINUTES=30
ALEMBIC_LOG_LEVEL=INFO
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARNING
+65
View File
@@ -0,0 +1,65 @@
FROM node:latest AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY overlord/package.json overlord/yarn.lock* overlord/package-lock.json* overlord/pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base as builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY /overlord ./
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM python:3.13 AS runner
LABEL maintainer="Amritanshu <docker@tanshu.com>"
RUN apt-get update && \
apt-get install -y locales && \
sed --in-place --expression='s/# en_IN UTF-8/en_IN UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
ENV LANG en_IN
ENV LC_ALL en_IN
# Install Poetry
RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python - && \
cd /usr/local/bin && \
ln -s /opt/poetry/bin/poetry && \
poetry config virtualenvs.create false
WORKDIR /app
COPY brewman/pyproject.toml /app/pyproject.toml
# Allow installing dev dependencies to run tests
ARG INSTALL_DEV=false
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi"
COPY /brewman ./
COPY --from=builder /frontend/browser /app/frontend
ENV PYTHONPATH=/app
EXPOSE 80
VOLUME /frontend
RUN chmod 777 /app/docker-entrypoint.sh \
&& ln -s /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh \
&& ln -s /app/docker-entrypoint.sh /
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["poetry", "run", "/app/run.sh"]
+7
View File
@@ -0,0 +1,7 @@
.PHONY: build-production
build-production: ## Build the production docker image.
@docker buildx build \
--platform linux/amd64,linux/arm64/v8 \
--tag registry.tanshu.com/brewman:latest \
--push \
git@git.tanshu.com:tanshu/brewman.git
+24
View File
@@ -19,3 +19,27 @@ rm -rf `poetry env info -p`
```sh
poetry install
```
### Setup python environment
```sh
# Install pyenv to manage python versions
curl https://pyenv.run | bash
# Install pyenv dependencies to build python (Optional)
sudo apt update; sudo apt install make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev curl git \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
# Install python 3.13
pyenv install 3.13
# Use python in the ~/programming/brewman/brewman directory
cd ~/programming/brewman/brewman
pyenv local 3.13
# Tell Poetry to use the pyenv Python
poetry env use python
# Or this
eval $(poetry env activate)
@@ -0,0 +1,341 @@
"""product versions
Revision ID: 52b98cebf96d
Revises: 82e5c8d18382
Create Date: 2025-06-08 04:16:11.152656
"""
import sqlalchemy as sa
from sqlalchemy import column, func, table, text
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision = "52b98cebf96d"
down_revision = "82e5c8d18382"
branch_labels = None
depends_on = None
def upgrade():
# 1. Create product_versions table
product_versions_schema()
# 2. Create sku_versions table
sku_versions_schema()
# 3. Migrate data from products to product_versions
product_versions_data()
# 4. Migrate data from stock_keeping_units to sku_versions
sku_versions_data()
def product_versions_schema():
op.create_table(
"product_versions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, default=sa.text("gen_random_uuid()")),
sa.Column("product_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("products.id"), nullable=False),
sa.Column("handle", sa.Unicode(), nullable=False),
sa.Column("name", sa.Unicode(), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("fraction_units", sa.Unicode(), nullable=False),
sa.Column(
"product_group_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("product_groups.id"), nullable=False
),
sa.Column("account_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("accounts.id"), nullable=False),
sa.Column("is_purchased", sa.Boolean(), nullable=False),
sa.Column("is_sold", sa.Boolean(), nullable=False),
sa.Column("allergen", sa.Text(), nullable=False),
sa.Column("protein", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("carbohydrate", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("total_sugar", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("added_sugar", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("total_fat", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("saturated_fat", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("trans_fat", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("cholestrol", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("sodium", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("msnf", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("other_solids", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("total_solids", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("water", sa.Numeric(precision=15, scale=5), nullable=False),
sa.Column("valid_from", sa.Date(), nullable=True),
sa.Column("valid_till", sa.Date(), nullable=True),
)
prod = table(
"product_versions",
column("id", postgresql.UUID(as_uuid=True)),
column("product_id", postgresql.UUID(as_uuid=True)),
column("name", sa.Unicode()),
column("handle", sa.Unicode()),
column("valid_from", sa.Date()),
column("valid_till", sa.Date()),
)
# Add exclusion constraints to product_versions
op.create_exclude_constraint(
"uq_product_versions_name",
"product_versions",
(prod.c.name, "="),
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"),
)
# Add exclusion constraints to product_versions
op.create_exclude_constraint(
"uq_product_versions_handle",
"product_versions",
(prod.c.handle, "="),
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"),
)
# Add exclusion constraints to product_versions
op.create_exclude_constraint(
"uq_product_versions_product_id",
"product_versions",
(prod.c.product_id, "="),
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"),
)
def product_versions_data():
products = sa.table(
"products",
sa.column("id", sa.String), # use UUID type if available
sa.column("code", sa.Integer),
sa.column("name", sa.String),
sa.column("description", sa.Text),
sa.column("fraction_units", sa.String),
sa.column("product_group_id", sa.String),
sa.column("account_id", sa.String),
sa.column("is_active", sa.Boolean),
sa.column("is_purchased", sa.Boolean),
sa.column("is_sold", sa.Boolean),
sa.column("allergen", sa.Text),
sa.column("protein", sa.Numeric(15, 5)),
sa.column("carbohydrate", sa.Numeric(15, 5)),
sa.column("total_sugar", sa.Numeric(15, 5)),
sa.column("added_sugar", sa.Numeric(15, 5)),
sa.column("total_fat", sa.Numeric(15, 5)),
sa.column("saturated_fat", sa.Numeric(15, 5)),
sa.column("trans_fat", sa.Numeric(15, 5)),
sa.column("cholestrol", sa.Numeric(15, 5)),
sa.column("sodium", sa.Numeric(15, 5)),
sa.column("msnf", sa.Numeric(15, 5)),
sa.column("other_solids", sa.Numeric(15, 5)),
sa.column("total_solids", sa.Numeric(15, 5)),
sa.column("water", sa.Numeric(15, 5)),
)
product_versions = sa.table(
"product_versions",
sa.column("id", sa.String), # use UUID type if available
sa.column("product_id", sa.String),
sa.column("handle", sa.Integer),
sa.column("name", sa.String),
sa.column("description", sa.Text),
sa.column("fraction_units", sa.String),
sa.column("product_group_id", sa.String),
sa.column("account_id", sa.String),
sa.column("is_purchased", sa.Boolean),
sa.column("is_sold", sa.Boolean),
sa.column("allergen", sa.Text),
sa.column("protein", sa.Numeric(15, 5)),
sa.column("carbohydrate", sa.Numeric(15, 5)),
sa.column("total_sugar", sa.Numeric(15, 5)),
sa.column("added_sugar", sa.Numeric(15, 5)),
sa.column("total_fat", sa.Numeric(15, 5)),
sa.column("saturated_fat", sa.Numeric(15, 5)),
sa.column("trans_fat", sa.Numeric(15, 5)),
sa.column("cholestrol", sa.Numeric(15, 5)),
sa.column("sodium", sa.Numeric(15, 5)),
sa.column("msnf", sa.Numeric(15, 5)),
sa.column("other_solids", sa.Numeric(15, 5)),
sa.column("total_solids", sa.Numeric(15, 5)),
sa.column("water", sa.Numeric(15, 5)),
sa.column("valid_from", sa.Date()),
sa.column("valid_till", sa.Date()),
)
insert_stmt = product_versions.insert().from_select(
[
"id",
"product_id",
"handle",
"name",
"description",
"fraction_units",
"product_group_id",
"account_id",
"is_purchased",
"is_sold",
"allergen",
"protein",
"carbohydrate",
"total_sugar",
"added_sugar",
"total_fat",
"saturated_fat",
"trans_fat",
"cholestrol",
"sodium",
"msnf",
"other_solids",
"total_solids",
"water",
"valid_from",
"valid_till",
],
sa.select(
sa.func.gen_random_uuid(), # id
products.c.id, # product_id
products.c.code, # handle
products.c.name,
products.c.description,
products.c.fraction_units,
products.c.product_group_id,
products.c.account_id,
products.c.is_purchased,
products.c.is_sold,
products.c.allergen,
products.c.protein,
products.c.carbohydrate,
products.c.total_sugar,
products.c.added_sugar,
products.c.total_fat,
products.c.saturated_fat,
products.c.trans_fat,
products.c.cholestrol,
products.c.sodium,
products.c.msnf,
products.c.other_solids,
products.c.total_solids,
products.c.water,
sa.null(), # valid_from
sa.case(
(products.c.is_active == sa.text("False"), sa.text("current_date - interval '1 day'")), else_=sa.null()
), # valid_till
),
)
op.execute(insert_stmt)
# 4. Drop migrated columns from products
columns_to_drop = [
"code",
"name",
"description",
"fraction_units",
"product_group_id",
"account_id",
"is_active",
"is_purchased",
"is_sold",
"allergen",
"protein",
"carbohydrate",
"total_sugar",
"added_sugar",
"total_fat",
"saturated_fat",
"trans_fat",
"cholestrol",
"sodium",
"msnf",
"other_solids",
"total_solids",
"water",
]
for col in columns_to_drop:
op.drop_column("products", col)
def sku_versions_schema():
op.create_table(
"sku_versions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("sku_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("stock_keeping_units.id"), nullable=False),
sa.Column("units", sa.Unicode(), nullable=False),
sa.Column("fraction", sa.Numeric(15, 5), nullable=False),
sa.Column("product_yield", sa.Numeric(15, 5), nullable=False),
sa.Column("cost_price", sa.Numeric(15, 2), nullable=False),
sa.Column("sale_price", sa.Numeric(15, 2), nullable=False),
sa.Column("valid_from", sa.Date(), nullable=True),
sa.Column("valid_till", sa.Date(), nullable=True),
)
sku = table(
"sku_versions",
column("id", postgresql.UUID(as_uuid=True)),
column("sku_id", postgresql.UUID(as_uuid=True)),
column("units", sa.Unicode()),
column("handle", sa.Unicode()),
column("valid_from", sa.Date()),
column("valid_till", sa.Date()),
)
# Add exclusion constraints to product_versions
op.create_exclude_constraint(
"uq_sku_versions_sku_id_units",
"sku_versions",
(sku.c.sku_id, "="),
(sku.c.units, "="),
(func.daterange(sku.c.valid_from, sku.c.valid_till, text("'[]'")), "&&"),
)
def sku_versions_data():
stock_keeping_units = sa.table(
"stock_keeping_units",
sa.column("id", postgresql.UUID(as_uuid=True)),
sa.column("product_id", postgresql.UUID(as_uuid=True)),
sa.column("units", sa.Unicode),
sa.column("fraction", sa.Numeric(15, 5)),
sa.column("product_yield", sa.Numeric(15, 5)),
sa.column("cost_price", sa.Numeric(15, 2)),
sa.column("sale_price", sa.Numeric(15, 2)),
)
sku_versions = sa.table(
"sku_versions",
sa.column("id", postgresql.UUID(as_uuid=True)),
sa.column("sku_id", postgresql.UUID(as_uuid=True)),
sa.column("units", sa.Unicode),
sa.column("fraction", sa.Numeric(15, 5)),
sa.column("product_yield", sa.Numeric(15, 5)),
sa.column("cost_price", sa.Numeric(15, 2)),
sa.column("sale_price", sa.Numeric(15, 2)),
sa.column("valid_from", sa.Date),
sa.column("valid_till", sa.Date),
)
insert_stmt = sku_versions.insert().from_select(
[
"id",
"sku_id",
"units",
"fraction",
"product_yield",
"cost_price",
"sale_price",
],
sa.select(
sa.func.gen_random_uuid(),
stock_keeping_units.c.id, # old SKU id is new sku_id
stock_keeping_units.c.units,
stock_keeping_units.c.fraction,
stock_keeping_units.c.product_yield,
stock_keeping_units.c.cost_price,
stock_keeping_units.c.sale_price,
),
)
op.execute(insert_stmt)
# 4. Drop versioned columns from stock_keeping_units
for col in ["units", "fraction", "product_yield", "cost_price", "sale_price"]:
op.drop_column("stock_keeping_units", col)
def downgrade():
# Reverse the above steps: not implemented for brevity, but you would
# 1. Add columns back to products,
# 2. Move data back from product_versions,
# 3. Drop new columns/constraints, drop product_versions table.
raise NotImplementedError("Downgrade not implemented")
+16 -2
View File
@@ -1,14 +1,16 @@
import secrets
from dotenv import load_dotenv
from pydantic import PostgresDsn, model_validator
from pydantic import PostgresDsn, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# openssl rand -hex 32
TITLE: str = ""
SECRET_KEY: str = secrets.token_urlsafe(32)
PUBLIC_KEY: str = secrets.token_urlsafe(32)
# openssl rand -hex 32
PRIVATE_KEY: str = secrets.token_urlsafe(32)
MIDDLEWARE_SECRET_KEY: str = secrets.token_urlsafe(5)
ALGORITHM: str = "HS256"
JWT_TOKEN_EXPIRE_MINUTES: int = 30
@@ -22,6 +24,18 @@ class Settings(BaseSettings):
POSTGRES_DB: str = ""
SQLALCHEMY_DATABASE_URI: str | None = None
@field_validator("PRIVATE_KEY", mode="before")
def convert_private_key_newlines(cls, v):
if isinstance(v, str):
return v.replace("\\n", "\n")
return v
@field_validator("PUBLIC_KEY", mode="before")
def convert_public_key_newlines(cls, v):
if isinstance(v, str):
return v.replace("\\n", "\n")
return v
@model_validator(mode="after")
def assemble_db_connection(self) -> "Settings":
if isinstance(self.SQLALCHEMY_DATABASE_URI, str):
+5 -5
View File
@@ -3,10 +3,10 @@ import uuid
from datetime import UTC, datetime, timedelta
from typing import Any
import jwt
from fastapi import Depends, HTTPException, Security, status
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from jose import jwt
from jose.exceptions import ExpiredSignatureError
from jwt import PyJWTError
from pydantic import BaseModel, ValidationError
from sqlalchemy import select
@@ -37,7 +37,7 @@ def create_access_token(*, data: dict[str, Any], expires_delta: timedelta | None
to_encode = data.copy()
expire = datetime.now(UTC).replace(tzinfo=None) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
encoded_jwt = jwt.encode(to_encode, settings.PRIVATE_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
@@ -80,13 +80,13 @@ async def get_current_user(
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
payload = jwt.decode(token, settings.PUBLIC_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub") or ""
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(scopes=token_scopes, username=username)
except (PyJWTError, ValidationError, ExpiredSignatureError):
except (PyJWTError, ValidationError):
raise credentials_exception
user = get_user(
username=token_data.username,
+1
View File
@@ -23,6 +23,7 @@ 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.product_version import ProductVersion # noqa: F401
from ..models.rate_contract import RateContract # noqa: F401
from ..models.rate_contract_item import RateContractItem # noqa: F401
from ..models.recipe import Recipe # noqa: F401
+2 -12
View File
@@ -1,25 +1,15 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
from sqlalchemy.orm import Mapped, relationship
from ..db.base_class import reg
from .account_base import AccountBase
if TYPE_CHECKING:
from .product import Product
@reg.mapped_as_dataclass(unsafe_hash=True)
class Account(AccountBase):
__mapper_args__ = {"polymorphic_identity": ""} # type: ignore
products: Mapped[list["Product"]] = relationship(
"Product", primaryjoin="Account.id==Product.account_id", back_populates="account"
)
def __init__(
self,
name: str,
+2 -2
View File
@@ -13,7 +13,7 @@ from .account_type import AccountType
if TYPE_CHECKING:
from .cost_centre import CostCentre
from .journal import Journal
from .product import Product
from .product_version import ProductVersion
from .rate_contract import RateContract
@@ -42,7 +42,7 @@ class AccountBase:
journals: Mapped[list["Journal"]] = relationship("Journal", back_populates="account")
cost_centre: Mapped["CostCentre"] = relationship("CostCentre", back_populates="accounts")
products: Mapped[list["Product"]] = relationship("Product", back_populates="account")
products: Mapped[list["ProductVersion"]] = relationship("ProductVersion", back_populates="account")
rate_contracts: Mapped[list["RateContract"]] = relationship("RateContract", back_populates="vendor")
@@ -0,0 +1,63 @@
from __future__ import annotations
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Date, DateTime, ForeignKey, Numeric, UniqueConstraint, Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .stock_keeping_unit import StockKeepingUnit
@reg.mapped_as_dataclass(unsafe_hash=True)
class MozimoOrderSheet:
__tablename__ = "mozimo_order_sheet"
__table_args__ = (UniqueConstraint("date", "sku_id"),)
id_: Mapped[uuid.UUID] = mapped_column("id", Uuid, primary_key=True, insert_default=uuid.uuid4)
date_: Mapped[date] = mapped_column("date", Date, nullable=False)
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
received: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
sale: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
nc: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
# The physical stock in the display area
display: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
# The physical stock in the ageing room
ageing: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
last_edit_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit")
def __init__(
self,
date_: date,
received: Decimal,
sale: Decimal,
nc: Decimal,
display: Decimal,
ageing: Decimal,
sku_id: uuid.UUID | None = None,
sku: StockKeepingUnit | None = None,
id_: uuid.UUID | None = None,
last_edit_date: datetime | None = None,
):
self.date_ = date_
self.received = received
self.sale = sale
self.nc = nc
self.display = display
self.ageing = ageing
if sku_id is not None:
self.sku_id = sku_id
if sku is not None:
self.sku = sku
self.sku_id = sku.id
if id_ is not None:
self.id_ = id_
self.last_edit_date = last_edit_date or datetime.now(UTC).replace(tzinfo=None)
@@ -27,21 +27,21 @@ class MozimoStockRegister:
sale: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
nc: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
# The physical stock in the display area
display: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
display: Mapped[Decimal | None] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
# The physical stock in the ageing room
ageing: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
ageing: Mapped[Decimal | None] = mapped_column(Numeric(precision=15, scale=2), nullable=True)
last_edit_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit")
def __init__(
self,
date_: datetime.date,
date_: date,
received: Decimal,
sale: Decimal,
nc: Decimal,
display: Decimal,
ageing: Decimal,
display: Decimal | None = None,
ageing: Decimal | None = None,
sku_id: uuid.UUID | None = None,
sku: StockKeepingUnit | None = None,
id_: uuid.UUID | None = None,
+6 -98
View File
@@ -1,119 +1,27 @@
import uuid
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, Text, Unicode, Uuid
from sqlalchemy import Boolean, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .account import Account
from .product_group import ProductGroup
from .product_version import ProductVersion
from .stock_keeping_unit import StockKeepingUnit
@reg.mapped_as_dataclass(unsafe_hash=True)
class Product:
__tablename__ = "products"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
code: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
name: Mapped[str] = mapped_column(Unicode, nullable=False, unique=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
fraction_units: Mapped[str] = mapped_column(Unicode, nullable=False)
product_group_id: Mapped[uuid.UUID] = mapped_column(
Uuid,
ForeignKey("product_groups.id"),
nullable=False,
)
account_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("accounts.id"), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
is_fixture: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_purchased: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_sold: Mapped[bool] = mapped_column(Boolean, nullable=False)
versions: Mapped[list["ProductVersion"]] = relationship(back_populates="product")
skus: Mapped[list["StockKeepingUnit"]] = relationship("StockKeepingUnit", back_populates="product")
product_group: Mapped["ProductGroup"] = relationship("ProductGroup", back_populates="products")
account: Mapped["Account"] = relationship("Account", back_populates="products")
allergen: Mapped[str] = mapped_column(Text, nullable=False)
protein: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
carbohydrate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
added_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
saturated_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
trans_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
cholestrol: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
sodium: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
msnf: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
other_solids: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_solids: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
water: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
def __init__(
self,
name: str,
description: str | None,
fraction_units: str,
product_group_id: uuid.UUID,
account_id: uuid.UUID,
is_active: bool,
is_purchased: bool,
is_sold: bool,
allergen: str = "",
protein: Decimal = Decimal(0),
carbohydrate: Decimal = Decimal(0),
total_sugar: Decimal = Decimal(0),
added_sugar: Decimal = Decimal(0),
total_fat: Decimal = Decimal(0),
saturated_fat: Decimal = Decimal(0),
trans_fat: Decimal = Decimal(0),
cholestrol: Decimal = Decimal(0),
sodium: Decimal = Decimal(0),
msnf: Decimal = Decimal(0),
other_solids: Decimal = Decimal(0),
total_solids: Decimal = Decimal(0),
water: Decimal = Decimal(0),
code: int | None = None,
id_: uuid.UUID | None = None,
is_fixture: bool | None = False,
) -> None:
if code is not None:
self.code = code
self.name = name
self.description = description
self.fraction_units = fraction_units
self.product_group_id = product_group_id
self.account_id = account_id
self.is_active = is_active
self.is_purchased = is_purchased
self.is_sold = is_sold
self.allergen = allergen
self.protein = protein
self.carbohydrate = carbohydrate
self.total_sugar = total_sugar
self.added_sugar = added_sugar
self.total_fat = total_fat
self.saturated_fat = saturated_fat
self.trans_fat = trans_fat
self.cholestrol = cholestrol
self.sodium = sodium
self.msnf = msnf
self.other_solids = other_solids
self.total_solids = total_solids
self.water = water
def __init__(self, id_: uuid.UUID | None = None, is_fixture: bool | None = None) -> None:
if id_ is not None:
self.id = id_
if is_fixture is not None:
self.is_fixture = is_fixture
@classmethod
def suspense(cls) -> uuid.UUID:
return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15")
self.is_fixture = is_fixture if is_fixture is not None else False
+2 -2
View File
@@ -9,7 +9,7 @@ from ..db.base_class import reg
if TYPE_CHECKING:
from .product import Product
from .product_version import ProductVersion
@reg.mapped_as_dataclass(unsafe_hash=True)
@@ -22,7 +22,7 @@ class ProductGroup:
ice_cream: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_fixture: Mapped[bool] = mapped_column(Boolean, nullable=False)
products: Mapped[list["Product"]] = relationship("Product", back_populates="product_group")
products: Mapped[list["ProductVersion"]] = relationship("ProductVersion", back_populates="product_group")
def __init__(
self,
+150
View File
@@ -0,0 +1,150 @@
import re
import uuid
from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, Date, ForeignKey, Numeric, Text, Unicode, Uuid, func, text
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .account import Account
from .product import Product
from .product_group import ProductGroup
@reg.mapped_as_dataclass(unsafe_hash=True)
class ProductVersion:
__tablename__ = "product_versions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False)
handle: Mapped[str] = mapped_column(Unicode, nullable=False)
name: Mapped[str] = mapped_column(Unicode, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
fraction_units: Mapped[str] = mapped_column(Unicode, nullable=False)
product_group_id: Mapped[uuid.UUID] = mapped_column(
Uuid,
ForeignKey("product_groups.id"),
nullable=False,
)
account_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("accounts.id"), nullable=False)
is_purchased: Mapped[bool] = mapped_column(Boolean, nullable=False)
is_sold: Mapped[bool] = mapped_column(Boolean, nullable=False)
product: Mapped["Product"] = relationship("Product", back_populates="versions")
product_group: Mapped["ProductGroup"] = relationship("ProductGroup", back_populates="products")
account: Mapped["Account"] = relationship("Account", back_populates="products")
allergen: Mapped[str] = mapped_column(Text, nullable=False)
protein: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
carbohydrate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
added_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
saturated_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
trans_fat: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
cholestrol: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
sodium: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
msnf: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
other_solids: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_solids: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
water: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
valid_from: Mapped[date | None] = mapped_column(Date(), nullable=True)
valid_till: Mapped[date | None] = mapped_column(Date(), nullable=True)
__table_args__ = (
postgresql.ExcludeConstraint(
(name, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
), # type: ignore[no-untyped-call]
postgresql.ExcludeConstraint(
(handle, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
), # type: ignore[no-untyped-call]
postgresql.ExcludeConstraint(
(product_id, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
), # type: ignore[no-untyped-call]
)
def __init__(
self,
product_id: uuid.UUID | None = None,
name: str = "",
description: str | None = None,
handle: str | None = None,
fraction_units: str = "",
product_group_id: uuid.UUID | None = None,
account_id: uuid.UUID | None = None,
is_purchased: bool = False,
is_sold: bool = False,
allergen: str = "",
protein: Decimal = Decimal(0),
carbohydrate: Decimal = Decimal(0),
total_sugar: Decimal = Decimal(0),
added_sugar: Decimal = Decimal(0),
total_fat: Decimal = Decimal(0),
saturated_fat: Decimal = Decimal(0),
trans_fat: Decimal = Decimal(0),
cholestrol: Decimal = Decimal(0),
sodium: Decimal = Decimal(0),
msnf: Decimal = Decimal(0),
other_solids: Decimal = Decimal(0),
total_solids: Decimal = Decimal(0),
water: Decimal = Decimal(0),
id_: uuid.UUID | None = None,
valid_from: date | None = None,
valid_till: date | None = None,
) -> None:
if product_id is not None:
self.product_id = product_id
self.name = name
self.description = description
self.handle = self.slugify(name) if handle is None else handle
self.fraction_units = fraction_units
if product_group_id is not None:
self.product_group_id = product_group_id
if account_id is not None:
self.account_id = account_id
self.is_purchased = is_purchased
self.is_sold = is_sold
self.allergen = allergen
self.protein = protein
self.carbohydrate = carbohydrate
self.total_sugar = total_sugar
self.added_sugar = added_sugar
self.total_fat = total_fat
self.saturated_fat = saturated_fat
self.trans_fat = trans_fat
self.cholestrol = cholestrol
self.sodium = sodium
self.msnf = msnf
self.other_solids = other_solids
self.total_solids = total_solids
self.water = water
if id_ is not None:
self.id = id_
self.valid_from = valid_from
self.valid_till = valid_till
@classmethod
def suspense(cls) -> uuid.UUID:
return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15")
@staticmethod
def slugify(value: str) -> str:
value = value.strip().lower()
value = re.sub(r"[^\w\s-]", "", value)
value = re.sub(r"[\s_-]+", "-", value)
value = re.sub(r"^-+|-+$", "", value)
return value
+81
View File
@@ -0,0 +1,81 @@
from __future__ import annotations
import uuid
from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import Date, ForeignKey, Numeric, Unicode, Uuid, func, text
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .stock_keeping_unit import StockKeepingUnit
@reg.mapped_as_dataclass(unsafe_hash=True)
class SkuVersion:
__tablename__ = "sku_versions"
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
# product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False) # should i remove this?
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
units: Mapped[str] = mapped_column(Unicode, nullable=False)
fraction: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
product_yield: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
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)
valid_from: Mapped[date | None] = mapped_column(Date(), nullable=True)
valid_till: Mapped[date | None] = mapped_column(Date(), nullable=True)
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit", back_populates="versions")
__table_args__ = (
# postgresql.ExcludeConstraint(
# (product_id, "="),
# (units, "="),
# (func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
# ), # type: ignore[no-untyped-call]
postgresql.ExcludeConstraint(
(sku_id, "="),
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
), # type: ignore[no-untyped-call]
)
def __init__(
self,
units: str = "",
fraction: Decimal = Decimal("1.0"),
product_yield: Decimal = Decimal("1.0"),
cost_price: Decimal = Decimal("0.0"),
sale_price: Decimal = Decimal("0.0"),
# product_id: uuid.UUID | None = None,
# product: Product | None = None,
sku_id: uuid.UUID | None = None,
sku: StockKeepingUnit | None = None,
id_: uuid.UUID | None = None,
valid_from: date | None = None,
valid_till: date | None = None,
) -> None:
# if product_id is not None:
# self.product_id = product_id
if sku_id is not None:
self.sku_id = sku_id
self.units = units
self.fraction = fraction
self.product_yield = product_yield
self.cost_price = cost_price
self.sale_price = sale_price
if id_ is not None:
self.id = id_
# if product is not None:
# self.product = product
if sku is not None:
self.sku = sku
self.valid_from = valid_from
self.valid_till = valid_till
+4 -19
View File
@@ -2,10 +2,9 @@ from __future__ import annotations
import uuid
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Numeric, Unicode, UniqueConstraint, Uuid
from sqlalchemy import ForeignKey, Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
@@ -15,43 +14,29 @@ if TYPE_CHECKING:
from .batch import Batch
from .product import Product
from .recipe import Recipe
from .sku_version import SkuVersion
@reg.mapped_as_dataclass(unsafe_hash=True)
class StockKeepingUnit:
__tablename__ = "stock_keeping_units"
__table_args__ = (UniqueConstraint("product_id", "units"),)
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), nullable=False)
units: Mapped[str] = mapped_column(Unicode, nullable=False)
fraction: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
product_yield: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
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)
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), unique=True, nullable=False)
product: Mapped[Product] = relationship("Product", back_populates="skus")
batches: Mapped[list[Batch]] = relationship("Batch", back_populates="sku")
recipes: Mapped[list[Recipe]] = relationship("Recipe", back_populates="sku")
versions: Mapped[list[SkuVersion]] = relationship(back_populates="sku")
def __init__(
self,
units: str,
fraction: Decimal,
product_yield: Decimal,
cost_price: Decimal,
sale_price: Decimal,
product_id: uuid.UUID | None = None,
product: Product | None = None,
id_: uuid.UUID | None = None,
) -> None:
if product_id is not None:
self.product_id = product_id
self.units = units
self.fraction = fraction
self.product_yield = product_yield
self.cost_price = cost_price
self.sale_price = sale_price
if id_ is not None:
self.id = id_
if product is not None:
+11 -1
View File
@@ -1,5 +1,5 @@
from collections.abc import Sequence
from datetime import date, timedelta
from datetime import UTC, date, datetime, timedelta
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
@@ -73,3 +73,13 @@ def get_lock_info(
return True, "Voucher allowed"
return False, "Default Fallthrough"
def effective_date(d: str | None = None) -> date:
return (
(
datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=330) # Adjust for IST (Indian Standard Time)
).date()
if d is None
else datetime.strptime(d, "%d-%b-%Y").date()
)
+38 -6
View File
@@ -1,11 +1,14 @@
import datetime
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import contains_eager
import brewman.schemas.batch as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.batch import Batch
@@ -26,21 +29,50 @@ def batch_term(
date_ = datetime.datetime.strptime(d, "%d-%b-%Y")
list_: list[schemas.Batch] = []
with SessionFuture() as db:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
query = (
select(Batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(SkuVersion, onclause=sku_version_onclause)
.where(Batch.quantity_remaining > 0, Batch.name <= date_)
.options(contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product))
.options(
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.versions),
)
)
if q is not None:
for q_item in q.split():
query = query.where(Product.name.ilike(f"%{q_item}%"))
result = db.execute(query.order_by(Product.name).order_by(Batch.name)).scalars().all()
query = query.where(ProductVersion.name.ilike(f"%{q_item}%"))
result = db.execute(query.order_by(ProductVersion.name).order_by(Batch.name)).unique().scalars().all()
for item in result:
product_name = item.sku.product.versions[0].name
product_units = item.sku.versions[0].units
text = (
f"{item.sku.product.name} ({item.sku.units}) {item.quantity_remaining:.2f}@"
f"{product_name} ({product_units}) {item.quantity_remaining:.2f}@"
f"{item.rate:.2f} from {item.name.strftime('%d-%b-%Y')}"
)
list_.append(
@@ -53,7 +85,7 @@ def batch_term(
discount=round(item.discount, 5),
sku=schemas.ProductLink(
id_=item.sku.id,
name=f"{item.sku.product.name} ({item.sku.units})",
name=f"{product_name} ({product_units})",
),
)
)
+31 -5
View File
@@ -3,11 +3,13 @@ import uuid
from decimal import Decimal
from fastapi import APIRouter, Security
from sqlalchemy import distinct, func, select, update
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy.orm import Session, contains_eager
import brewman.schemas.batch_integrity as schemas
from brewman.models.product_version import ProductVersion
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.batch import Batch
@@ -37,17 +39,29 @@ def post_check_batch_integrity(
def negative_batches(db: Session) -> list[schemas.BatchIntegrity]:
inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity")
list_ = db.execute(
select(Batch, Product.name, inv_sum)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
list_: list[tuple[Batch, str, Decimal]] = db.execute(
select(Batch, ProductVersion.name, inv_sum)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(Batch.inventories)
.join(Inventory.voucher)
.join(Voucher.journals)
.where(
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
.group_by(Batch, Product.name) # type: ignore
.group_by(Batch, ProductVersion.name) # type: ignore
.having(Batch.quantity_remaining != inv_sum)
).all()
@@ -93,11 +107,23 @@ def batch_details(batch_id: uuid.UUID, db: Session) -> list[schemas.BatchIntegri
def batch_dates(db: Session) -> list[schemas.BatchIntegrity]:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
list_ = (
db.execute(
select(Batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(Batch.inventories)
.join(Inventory.voucher)
.where(Voucher.date_ < Batch.name)
@@ -116,7 +142,7 @@ def batch_dates(db: Session) -> list[schemas.BatchIntegrity]:
issue.append(
schemas.BatchIntegrity(
id_=batch.id,
product=batch.sku.product.name,
product=batch.sku.product.versions[0].name,
date_=batch.name,
showing=batch.quantity_remaining,
actual=Decimal(0),
+58 -14
View File
@@ -3,7 +3,7 @@ import uuid
from decimal import Decimal
from fastapi import HTTPException, status
from sqlalchemy import distinct, func, select
from sqlalchemy import and_, distinct, func, or_, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
@@ -14,6 +14,7 @@ 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.sku_version import SkuVersion
from brewman.models.voucher import Voucher
from brewman.models.voucher_type import VoucherType
@@ -52,16 +53,29 @@ def calculate_prices(period_id: uuid.UUID, db: Session) -> None:
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_quantity = func.sum(Inventory.quantity * SkuVersion.fraction * SkuVersion.product_yield * Journal.debit).label(
"quantity"
)
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
issue_price = (sum_net / sum_quantity).label("issue_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
d: dict[uuid.UUID, Decimal] = {}
query = db.execute(
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
select(StockKeepingUnit.product_id, issue_price)
.select_from(Inventory)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(Inventory.voucher)
.join(Voucher.journals)
.where(
@@ -79,16 +93,30 @@ def get_issue_prices(period: Period, products: set[uuid.UUID], db: Session) -> 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_quantity = func.sum(Inventory.quantity * SkuVersion.fraction * SkuVersion.product_yield * Journal.debit).label(
"quantity"
)
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
purchase_price = (sum_net / sum_quantity).label("purchase_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
d: dict[uuid.UUID, Decimal] = {}
query = db.execute(
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
select(StockKeepingUnit.product_id, purchase_price)
.select_from(Inventory)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(Inventory.voucher)
.join(Voucher.journals)
.where(
@@ -105,12 +133,26 @@ def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dic
def get_rest(req: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
rest_price = (SkuVersion.cost_price / (SkuVersion.fraction * SkuVersion.product_yield)).label("rest_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
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))
rest_price,
)
.join(SkuVersion, onclause=sku_version_onclause)
.where(StockKeepingUnit.product_id.in_(req))
).all()
for id, amount in query:
d[id] = amount
@@ -127,6 +169,8 @@ def calculate_recipes(recipes: set[uuid.UUID], prices: dict[uuid.UUID, Decimal],
.all()
)
for item in items:
cost = sum(i.quantity * prices[i.product_id] for i in item.items) / (item.recipe_yield * item.sku.fraction)
cost = sum(i.quantity * prices[i.product_id] for i in item.items) / (
item.recipe_yield * item.sku.versions[-1].fraction
)
prices[item.sku.product_id] = cost
recipes.remove(item.sku.product_id)
@@ -48,7 +48,7 @@ def employee_attendance_report(
finish_date = datetime.strptime(f or get_finish_date(request.session), "%d-%b-%Y").date()
start_date = employee.joining_date if employee.joining_date > start_date else start_date
finish_date = (
employee.leaving_date if not employee.is_active and employee.leaving_date < finish_date else finish_date # type: ignore[assignment, operator]
employee.leaving_date if not employee.is_active and employee.leaving_date < finish_date else finish_date # type: ignore[assignment]
)
info = schemas.EmployeeAttendance(
start_date=start_date,
+1 -1
View File
@@ -95,7 +95,7 @@ def fp(file_data: StringIO, employees: dict[int, uuid.UUID]) -> list[schemas.Fin
def get_prints(employee_id: uuid.UUID, date_: date, db: Session) -> tuple[str, str, bool]:
prints = (
prints = list(
db.execute(
select(Fingerprint)
.where(
+104 -11
View File
@@ -4,13 +4,16 @@ from datetime import UTC, datetime
from decimal import Decimal
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import distinct, func, or_, select, update
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, contains_eager
import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, set_date
from ..db.session import SessionFuture
@@ -69,9 +72,25 @@ def save_route(
def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> tuple[Voucher, bool | None]:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(Product.account_id)
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
contains_eager(Product.skus),
)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
)
account_types: list[int] = list(
@@ -116,16 +135,45 @@ def save_inventories(
amount: Decimal = Decimal(0)
for data_item in inventories:
batch = db.execute(select(Batch).where(Batch.id == data_item.batch.id_)).scalar_one()
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == batch.sku_id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
if batch_consumed and data_item.quantity > batch.quantity_remaining:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{batch.sku.product.name} ({batch.sku.units}) stock is {batch.quantity_remaining}",
detail=f"{product_name} ({product_unit}) stock is {batch.quantity_remaining}",
)
if batch.name > voucher.date_:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{batch.sku.product.name} ({batch.sku.units}) "
f"was purchased on {batch.name.strftime('%d-%b-%Y')}",
detail=f"{product_name} ({product_unit}) was purchased on {batch.name.strftime('%d-%b-%Y')}",
)
if batch_consumed is None:
pass
@@ -205,9 +253,25 @@ def update_voucher(
id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session
) -> tuple[Voucher, bool | None]:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(Product.account_id)
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
contains_eager(Product.skus),
)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
)
account_types: list[int] = list(
@@ -280,18 +344,47 @@ def update_inventories(
index = next(
(idx for (idx, d) in enumerate(inventories) if d.id_ == item.id and d.batch.id_ == item.batch.id), None
)
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == batch.sku_id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
if index is not None:
new_inventory = inventories.pop(index)
if batch_consumed and new_inventory.quantity > batch_quantity:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Maximum quantity available for "
f"{batch.sku.product.name} ({batch.sku.units}) is {batch_quantity}",
detail=f"Maximum quantity available for {product_name} ({product_unit}) is {batch_quantity}",
)
if item.batch.name > voucher.date_:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Batch of {batch.sku.product.name} ({batch.sku.units}) was purchased after the issue date",
detail=f"Batch of {product_name} ({product_unit}) was purchased after the issue date",
)
if batch_consumed is None:
@@ -315,7 +408,7 @@ def update_inventories(
if batch_quantity < item.quantity:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Product {batch.sku.product.name} ({batch.sku.units}) cannot be removed,"
detail=f"Product {product_name} ({product_unit}) cannot be removed,"
f" minimum quantity is {batch_quantity}",
)
item.batch.quantity_remaining = batch_quantity
+391 -127
View File
@@ -1,25 +1,30 @@
import uuid
from datetime import datetime
from collections.abc import Sequence
from datetime import date, timedelta
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import delete, desc, func, or_, select
from sqlalchemy import and_, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
import brewman.schemas.product as schemas
from brewman.models.sku_version import SkuVersion
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.account import Account
from ..models.batch import Batch
from ..models.product import Product
from ..models.product_group import ProductGroup
from ..models.product_version import ProductVersion
from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem
from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas.product_sku import ProductSku
from ..schemas.user import UserToken
from . import effective_date
router = APIRouter()
@@ -32,13 +37,17 @@ def save(
) -> schemas.Product:
try:
with SessionFuture() as db:
item = Product(
product = Product()
db.add(product)
db.flush() # So product.id is available
version = ProductVersion(
product_id=product.id,
name=data.name,
description=data.description,
fraction_units=data.fraction_units,
product_group_id=data.product_group.id_,
account_id=Account.all_purchases(),
is_active=data.is_active,
is_purchased=data.is_purchased,
is_sold=data.is_sold,
allergen=data.allergen,
@@ -55,27 +64,34 @@ def save(
other_solids=data.other_solids,
total_solids=data.total_solids,
water=data.water,
valid_from=None,
valid_till=None,
)
item.code = db.execute(select(func.coalesce(func.max(Product.code), 0) + 1)).scalar_one()
db.add(item)
db.add(version)
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
for sku in data.skus:
db.add(
StockKeepingUnit(
units=sku.units,
fraction=round(sku.fraction, 5),
product_yield=round(sku.product_yield, 5),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
product=item,
)
s = StockKeepingUnit(product=product)
product.skus.append(s)
db.add(s)
db.flush() # So sku.id is available
sver = SkuVersion(
units=sku.units,
fraction=round(sku.fraction, 5),
product_yield=round(sku.product_yield, 5),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
sku=s,
)
s.versions.append(sver)
db.add(sver)
db.commit()
return product_info(item)
db.refresh(product)
return product_info(version, [s.versions[0] for s in product.skus])
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -87,77 +103,184 @@ def save(
def update_route(
id_: uuid.UUID,
data: schemas.ProductIn,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.Product:
try:
with SessionFuture() as db:
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
if item.is_fixture:
version: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one()
if version.product.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
detail=f"{version.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.description = data.description
item.fraction_units = data.fraction_units
item.product_group_id = data.product_group.id_
item.account_id = Account.all_purchases()
item.is_active = data.is_active
item.is_purchased = data.is_purchased
item.is_sold = data.is_sold
item.allergen = data.allergen
item.protein = data.protein
item.carbohydrate = data.carbohydrate
item.total_sugar = data.total_sugar
item.added_sugar = data.added_sugar
item.total_fat = data.total_fat
item.saturated_fat = data.saturated_fat
item.trans_fat = data.trans_fat
item.cholestrol = data.cholestrol
item.sodium = data.sodium
item.msnf = data.msnf
item.other_solids = data.other_solids
item.total_solids = data.total_solids
item.water = data.water
changed = (
data.name != version.name
or data.description != version.description
or data.fraction_units != version.fraction_units
or data.is_purchased != version.is_purchased
or data.is_sold != version.is_sold
or data.allergen != version.allergen
or Decimal(data.protein).quantize(Decimal("0.00001")) != version.protein
or Decimal(data.carbohydrate).quantize(Decimal("0.00001")) != version.carbohydrate
or Decimal(data.total_sugar).quantize(Decimal("0.00001")) != version.total_sugar
or Decimal(data.added_sugar).quantize(Decimal("0.00001")) != version.added_sugar
or Decimal(data.total_fat).quantize(Decimal("0.00001")) != version.total_fat
or Decimal(data.saturated_fat).quantize(Decimal("0.00001")) != version.saturated_fat
or Decimal(data.trans_fat).quantize(Decimal("0.00001")) != version.trans_fat
or Decimal(data.cholestrol).quantize(Decimal("0.00001")) != version.cholestrol
or Decimal(data.sodium).quantize(Decimal("0.00001")) != version.sodium
or Decimal(data.msnf).quantize(Decimal("0.00001")) != version.msnf
or Decimal(data.other_solids).quantize(Decimal("0.00001")) != version.other_solids
or Decimal(data.total_solids).quantize(Decimal("0.00001")) != version.total_solids
or Decimal(data.water).quantize(Decimal("0.00001")) != version.water
or version.product_group_id != data.product_group.id_
or version.account_id != Account.all_purchases()
)
if changed:
new_version = (
version if version.valid_from == date_ else ProductVersion(product_id=id_, valid_from=date_)
)
new_version.handle = ProductVersion.slugify(data.name)
new_version.name = data.name
new_version.description = data.description
new_version.fraction_units = data.fraction_units
new_version.product_group_id = data.product_group.id_
new_version.account_id = Account.all_purchases()
new_version.is_purchased = data.is_purchased
new_version.is_sold = data.is_sold
new_version.allergen = data.allergen
new_version.protein = data.protein
new_version.carbohydrate = data.carbohydrate
new_version.total_sugar = data.total_sugar
new_version.added_sugar = data.added_sugar
new_version.total_fat = data.total_fat
new_version.saturated_fat = data.saturated_fat
new_version.trans_fat = data.trans_fat
new_version.cholestrol = data.cholestrol
new_version.sodium = data.sodium
new_version.msnf = data.msnf
new_version.other_solids = data.other_solids
new_version.total_solids = data.total_solids
new_version.water = data.water
if version.valid_from != date_:
version.valid_till = date_ - timedelta(days=1)
version.product.versions.append(new_version)
db.add(new_version)
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
for i in range(len(item.skus), 0, -1):
sku = item.skus[i - 1]
old_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
)
for i in range(len(old_skus), 0, -1):
sku: SkuVersion = old_skus[i - 1]
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None)
if index is not None:
new_data_sku = data.skus.pop(index)
sku.units = new_data_sku.units
sku.fraction = round(new_data_sku.fraction, 5)
sku.product_yield = round(new_data_sku.product_yield, 5)
sku.cost_price = round(new_data_sku.cost_price, 2)
sku.sale_price = round(new_data_sku.sale_price, 2)
else:
count: int = db.execute(select(func.count()).where(Batch.sku_id == sku.id)).scalar_one()
if count > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"SKU '{sku.units}' has entries and cannot be deleted.",
sku_changed = (
new_data_sku.units != sku.units
or Decimal(new_data_sku.fraction).quantize(Decimal("0.00001")) != sku.fraction
or Decimal(new_data_sku.product_yield).quantize(Decimal("0.00001")) != sku.product_yield
or Decimal(new_data_sku.cost_price).quantize(Decimal("0.01")) != sku.cost_price
or Decimal(new_data_sku.sale_price).quantize(Decimal("0.01")) != sku.sale_price
)
print(f"SKU Changed: {sku_changed} for {sku.id}")
if sku_changed:
new_sku = (
sku
if sku.valid_from == date_
else SkuVersion(
sku_id=sku.sku_id,
valid_from=date_,
)
)
item.skus.remove(sku)
db.delete(sku)
new_sku.units = new_data_sku.units
new_sku.fraction = round(new_data_sku.fraction, 5)
new_sku.product_yield = round(new_data_sku.product_yield, 5)
new_sku.cost_price = round(new_data_sku.cost_price, 2)
new_sku.sale_price = round(new_data_sku.sale_price, 2)
if sku.valid_from != date_:
sku.valid_till = date_ - timedelta(days=1)
db.add(new_sku)
else:
sku.valid_till = date_ - timedelta(days=1)
for data_sku in data.skus:
new_sku = StockKeepingUnit(
new_s = StockKeepingUnit(product_id=version.product_id)
version.product.skus.append(new_s)
db.add(new_s)
db.flush() # So sku.id is available
new_sku = SkuVersion(
units=data_sku.units,
fraction=round(data_sku.fraction, 5),
product_yield=round(data_sku.product_yield, 5),
cost_price=round(data_sku.cost_price, 2),
sale_price=round(data_sku.sale_price, 2),
product=item,
sku=new_s,
)
db.add(new_sku)
item.skus.append(new_sku)
current_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
)
db.commit()
return product_info(item)
return product_info(version, current_skus)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -168,6 +291,7 @@ def update_route(
@router.delete("/{id_}", response_model=schemas.ProductBlank)
def delete_route(
id_: uuid.UUID,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.ProductBlank:
with SessionFuture() as db:
@@ -175,17 +299,49 @@ def delete_route(
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
detail="This product is a fixture and cannot be edited or deleted.",
)
version: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
count: int = db.execute(
select(func.count()).join(StockKeepingUnit.batches).where(StockKeepingUnit.product_id == id_)
).scalar_one()
if count > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Product has entries and cannot be deleted."
skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
db.execute(delete(StockKeepingUnit).where(StockKeepingUnit.product_id == id_))
db.execute(delete(Product).where(Product.id == id_))
.scalars()
.all()
)
valid_till = date_ - timedelta(days=1)
version.valid_till = valid_till
for sku in skus:
sku.valid_till = valid_till
db.commit()
return product_blank()
@@ -198,20 +354,45 @@ def show_blank(
@router.get("/list", response_model=list[schemas.Product])
def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Product]:
def show_list(date_: date = Depends(effective_date), user: UserToken = Depends(get_user)) -> list[schemas.Product]:
with SessionFuture() as db:
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
return [
product_info(item)
product_info(item, [s.versions[0] for s in item.product.skus])
for item in db.execute(
select(Product)
.join(Product.product_group)
select(ProductVersion)
.join(ProductVersion.product)
.join(ProductVersion.product_group)
.join(ProductVersion.product)
.join(Product.skus)
.order_by(desc(Product.is_active))
.order_by(Product.product_group_id)
.order_by(Product.name)
.join(SkuVersion, onclause=sku_version_onclause)
.where(
and_(
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
.order_by(ProductGroup.name)
.order_by(ProductVersion.name)
.options(
contains_eager(Product.skus),
contains_eager(Product.product_group),
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
)
.unique()
@@ -226,30 +407,64 @@ async def show_term_sku(
a: bool | None = None, # Active
p: bool | None = None, # Is Purchased?
v: uuid.UUID | None = None, # Vendor
d: str | None = None, # Date
date_: date = Depends(effective_date), # Date
current_user: UserToken = Depends(get_user),
) -> list[ProductSku]:
list_ = []
with SessionFuture() as db:
query_ = select(Product).join(Product.skus).options(contains_eager(Product.skus))
if a is not None:
query_ = query_.filter(Product.is_active == a)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
query_ = (
select(ProductVersion)
.join(ProductVersion.product)
.join(ProductVersion.product_group)
.join(ProductVersion.product)
.join(Product.skus)
.join(SkuVersion, onclause=sku_version_onclause)
.where(
and_(
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
)
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(Product.is_purchased == p)
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(
or_(Product.name.ilike(f"%{sub}%"), StockKeepingUnit.units.ilike(f"%{sub}%"))
or_(ProductVersion.name.ilike(f"%{sub}%"), SkuVersion.units.ilike(f"%{sub}%"))
)
query_ = query_.order_by(Product.name)
query_ = query_.order_by(ProductVersion.name)
query_ = query_.options(
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
for item in db.execute(query_).unique().scalars().all():
for sku in item.skus:
rc_price = get_rc_price(item.id, d, v, db)
for sku in [sku.versions[0] for sku in item.product.skus]:
rc_price = get_rc_price(item.id, date_, v, db)
list_.append(
ProductSku(
id_=sku.id,
id_=sku.sku_id,
name=f"{item.name} ({sku.units})",
fraction_units=item.fraction_units,
cost_price=sku.cost_price if rc_price is None else rc_price,
@@ -265,20 +480,33 @@ async def show_term_product(
q: str | None = None, # Query
a: bool | None = None, # Active
p: bool | None = None, # Is Purchased?
date_: date = Depends(effective_date), # Date
current_user: UserToken = Depends(get_user),
) -> list[ProductSku]:
list_ = []
with SessionFuture() as db:
query_ = select(Product)
if a is not None:
query_ = query_.filter(Product.is_active == a)
query_ = select(ProductVersion).where(
and_(
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(Product.is_purchased == p)
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(Product.name.ilike(f"%{sub}%"))
query_ = query_.order_by(Product.name)
query_ = query_.filter(ProductVersion.name.ilike(f"%{sub}%"))
query_ = query_.order_by(ProductVersion.name)
for item in db.execute(query_).unique().scalars().all():
list_.append(
@@ -297,20 +525,57 @@ async def show_term_product(
@router.get("/{id_}", response_model=schemas.Product)
def show_id(
id_: uuid.UUID,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
) -> schemas.Product:
with SessionFuture() as db:
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
return product_info(item)
version: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one()
skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
)
return product_info(version, skus)
def product_info(product: Product) -> schemas.Product:
def product_info(version: ProductVersion, skus: Sequence[SkuVersion]) -> schemas.Product:
return schemas.Product(
id_=product.id,
code=product.code,
name=product.name,
description=product.description,
fraction_units=product.fraction_units,
id_=version.product_id,
handle=version.handle,
name=version.name,
description=version.description,
fraction_units=version.fraction_units,
skus=[
schemas.StockKeepingUnit(
id_=sku.id,
@@ -320,27 +585,27 @@ def product_info(product: Product) -> schemas.Product:
cost_price=sku.cost_price,
sale_price=sku.sale_price,
)
for sku in product.skus
for sku in skus
],
is_active=product.is_active,
is_fixture=product.is_fixture,
is_purchased=product.is_purchased,
is_sold=product.is_sold,
product_group=schemas.ProductGroupLink(id_=product.product_group.id, name=product.product_group.name),
allergen=product.allergen,
protein=product.protein,
carbohydrate=product.carbohydrate,
total_sugar=product.total_sugar,
added_sugar=product.added_sugar,
total_fat=product.total_fat,
saturated_fat=product.saturated_fat,
trans_fat=product.trans_fat,
cholestrol=product.cholestrol,
sodium=product.sodium,
msnf=product.msnf,
other_solids=product.other_solids,
total_solids=product.total_solids,
water=product.water,
is_fixture=version.product.is_fixture,
is_purchased=version.is_purchased,
is_sold=version.is_sold,
is_active=True,
product_group=schemas.ProductGroupLink(id_=version.product_group.id, name=version.product_group.name),
allergen=version.allergen,
protein=version.protein,
carbohydrate=version.carbohydrate,
total_sugar=version.total_sugar,
added_sugar=version.added_sugar,
total_fat=version.total_fat,
saturated_fat=version.saturated_fat,
trans_fat=version.trans_fat,
cholestrol=version.cholestrol,
sodium=version.sodium,
msnf=version.msnf,
other_solids=version.other_solids,
total_solids=version.total_solids,
water=version.water,
)
@@ -350,10 +615,10 @@ def product_blank() -> schemas.ProductBlank:
description="",
fraction_units="",
skus=[],
is_active=True,
is_purchased=True,
is_sold=False,
is_fixture=False,
is_active=True,
allergen="",
protein=Decimal(0),
carbohydrate=Decimal(0),
@@ -371,10 +636,9 @@ def product_blank() -> schemas.ProductBlank:
)
def get_rc_price(id_: uuid.UUID, d: str | None, v: uuid.UUID | None, db: Session) -> Decimal | None:
if d is None or v is None:
def get_rc_price(id_: uuid.UUID, date_: date, v: uuid.UUID | None, db: Session) -> Decimal | None:
if v is None:
return None
date_ = datetime.strptime(d, "%d-%b-%Y")
contracts = select(RateContract.id).where(
RateContract.vendor_id == v, RateContract.valid_from <= date_, RateContract.valid_till >= date_
)
+113 -17
View File
@@ -4,13 +4,15 @@ from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import distinct, func, or_, select
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from brewman.routers.tag import save_tags, update_tags
from ..core.security import get_current_active_user as get_user
@@ -73,10 +75,21 @@ def save_route(
def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
product_accounts = (
select(Product.account_id)
.join(Product.skus)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
product_ids = select(func.distinct(StockKeepingUnit.product_id)).where(
StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories])
)
product_accounts = select(func.distinct(ProductVersion.account_id)).where(
and_(
ProductVersion.product_id.in_(product_ids),
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
)
account_types: list[int] = list(
db.execute(
@@ -138,7 +151,23 @@ def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
tax=item.tax,
discount=item.discount,
)
sku.cost_price = item.rate
db.execute(
update(SkuVersion)
.where(
and_(
SkuVersion.sku_id == sku.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
.values(cost_price=item.rate)
)
voucher.inventories.append(inventory)
db.add(inventory)
@@ -148,11 +177,22 @@ def save_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) ->
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for i_item in voucher.inventories:
product_id = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id == i_item.batch.sku.id)
product_account = select(ProductVersion.account_id).where(
and_(
ProductVersion.product_id == product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product.skus)
.where(StockKeepingUnit.id == i_item.batch.sku.id)
select(AccountBase.id, AccountBase.cost_centre_id).where(AccountBase.id == product_account)
).one()
amount += round(i_item.amount, 2)
if account_id in journals:
@@ -205,11 +245,23 @@ def update_route(
def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_accounts = (
select(Product.account_id)
.join(Product.skus)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
product_ids = select(func.distinct(StockKeepingUnit.product_id)).where(
StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories])
)
product_accounts = select(func.distinct(ProductVersion.account_id)).where(
and_(
ProductVersion.product_id.in_(product_ids),
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
)
account_types: list[int] = list(
db.execute(
select(distinct(AccountBase.type_id)).where(
@@ -247,16 +299,32 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
(idx for (idx, d) in enumerate(inventories) if d.id_ == item.id and d.batch.sku.id_ == item.batch.sku_id),
None,
)
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == item.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
if index is not None:
new_inventory = inventories.pop(index)
sku: StockKeepingUnit = db.execute(
select(StockKeepingUnit).where(StockKeepingUnit.id == item.batch.sku.id)
).scalar_one()
rc_price = rate_contract_price(sku.id, vendor_id, voucher.date_, db)
if batch_has_older_vouchers(item.batch_id, voucher.date_, voucher.id, db):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{item.batch.sku.product.name} has older vouchers",
detail=f"{product_name} has older vouchers",
)
if rc_price is not None and rc_price != new_inventory.rate:
raise HTTPException(
@@ -280,7 +348,23 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
item.batch.discount = new_inventory.discount
item.tax = new_inventory.tax
item.batch.tax = new_inventory.tax
sku.cost_price = new_inventory.rate
db.execute(
update(SkuVersion)
.where(
and_(
SkuVersion.sku_id == sku.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
.values(cost_price=new_inventory.rate)
)
db.flush()
fix_single_batch_prices(item.batch_id, db)
else:
@@ -290,7 +374,7 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
if has_been_issued > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{item.batch.sku.product.name} has been issued, it cannot be deleted",
detail=f"{product_name} has been issued, it cannot be deleted",
)
else:
db.delete(item.batch)
@@ -304,9 +388,21 @@ def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) -
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for i_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.where(StockKeepingUnit.id == i_item.batch.sku.id)
).one()
+126 -9
View File
@@ -4,13 +4,15 @@ from datetime import UTC, datetime
from decimal import Decimal
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import distinct, or_, select
from sqlalchemy import and_, distinct, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, contains_eager
import brewman.schemas.input as schema_in
import brewman.schemas.voucher as output
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from brewman.routers.tag import save_tags, update_tags
from ..core.security import get_current_active_user as get_user
@@ -70,9 +72,25 @@ def save_route(
def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(Product.account_id)
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
contains_eager(Product.skus),
)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
)
account_types: list[int] = list(
@@ -107,17 +125,46 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
def save_inventories(voucher: Voucher, inventories: list[InventorySchema], db: Session) -> None:
for d_item in inventories:
batch = db.execute(select(Batch).where(Batch.id == d_item.batch.id_)).scalar_one()
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == batch.sku_id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
if d_item.quantity > batch.quantity_remaining:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{batch.sku.product.name} ({batch.sku.units}) stock is {batch.quantity_remaining}",
detail=f"{product_name} ({product_unit}) stock is {batch.quantity_remaining}",
)
if batch.name > voucher.date_:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"{batch.sku.product.name} ({batch.sku.units}) "
f"was purchased on {batch.name.strftime('%d-%b-%Y')}",
detail=f"{product_name} ({product_unit}) was purchased on {batch.name.strftime('%d-%b-%Y')}",
)
batch.quantity_remaining -= d_item.quantity
@@ -138,9 +185,21 @@ def save_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) ->
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for v_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.where(StockKeepingUnit.id == v_item.batch.sku.id)
).one()
@@ -199,11 +258,28 @@ def update_route(
def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(Product.account_id)
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
contains_eager(Product.skus),
)
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
)
account_types: list[int] = list(
db.execute(
select(distinct(AccountBase.type_id)).where(
@@ -240,13 +316,42 @@ def update_inventory(voucher: Voucher, new_inventories: list[InventorySchema], d
index = next(
(idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id and d.batch.id_ == item.batch.id), None
)
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == batch.sku_id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
if index is not None:
new_inventory = new_inventories.pop(index)
if new_inventory.quantity > batch_quantity:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Maximum quantity available for "
f"{batch.sku.product.name} ({batch.sku.units}) is {batch_quantity}",
detail=f"Maximum quantity available for {product_name} ({product_unit}) is {batch_quantity}",
)
if batch.name > voucher.date_:
raise HTTPException(
@@ -270,9 +375,21 @@ def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) -
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for v_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.where(StockKeepingUnit.id == v_item.batch.sku.id)
).one()
+47 -9
View File
@@ -1,12 +1,17 @@
import uuid
from datetime import date
from typing import Any
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import delete, select
from sqlalchemy import and_, delete, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from brewman.models.stock_keeping_unit import StockKeepingUnit
from ..core.security import get_current_active_user as get_user
from ..core.session import (
get_date,
@@ -50,7 +55,7 @@ async def save(
set_date(data.date_, request.session)
set_period(data.valid_from, data.valid_till, request.session)
db.commit()
return rate_contract_info(item)
return rate_contract_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -85,7 +90,7 @@ async def update_route(
set_date(data.date_, request.session)
set_period(data.valid_from, data.valid_till, request.session)
db.commit()
return rate_contract_info(item)
return rate_contract_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -134,7 +139,7 @@ async def show_list(
) -> list[RateContractSchema]:
with SessionFuture() as db:
return [
rate_contract_info(item)
rate_contract_info(item, db)
for item in db.execute(select(RateContract).order_by(RateContract.date_)).scalars().all()
]
@@ -146,10 +151,10 @@ async def show_id(
) -> RateContractSchema:
with SessionFuture() as db:
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
return rate_contract_info(item)
return rate_contract_info(item, db)
def rate_contract_info(item: RateContract) -> RateContractSchema:
def rate_contract_info(item: RateContract, db: Session) -> RateContractSchema:
return RateContractSchema(
id_=item.id,
date_=item.date_,
@@ -163,9 +168,7 @@ def rate_contract_info(item: RateContract) -> RateContractSchema:
items=[
RateContractItemSchema(
id_=i.id,
sku=ProductLink(
id_=i.sku_id, name=f"{i.sku.product.name} ({i.sku.units})" if i.sku.units else i.sku.product.name
),
sku=ProductLink(id_=i.sku_id, name=get_product_name(i.sku, item.date_, db)),
price=i.price,
)
for i in item.items
@@ -173,6 +176,41 @@ def rate_contract_info(item: RateContract) -> RateContractSchema:
)
def get_product_name(sku: StockKeepingUnit, date_: date, db: Session) -> str:
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == sku.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
).scalar_one()
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one()
return f"{product_name} ({product_unit})"
def rate_contract_blank(session: dict[str, Any]) -> RateContractBlank:
return RateContractBlank(
date_=get_date(session), # type: ignore[arg-type]
+2 -2
View File
@@ -108,7 +108,7 @@ def save_starred(date_: date, db: Session) -> list[uuid.UUID]:
def opening_accounts(date_: date, user_id: uuid.UUID, db: Session) -> Voucher:
running_total = 0
running_total = Decimal(0)
sum_func = func.sum(Journal.signed_amount)
query = db.execute(
select(AccountBase, sum_func)
@@ -134,7 +134,7 @@ def opening_accounts(date_: date, user_id: uuid.UUID, db: Session) -> Voucher:
)
for account, amount in query:
amount = round(Decimal(amount), 2)
if account.type_.balance_sheet and amount != 0:
if account.type_.balance_sheet and amount != Decimal(0):
running_total += amount
journal = Journal(
amount=abs(amount),
+115 -24
View File
@@ -12,13 +12,16 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from fastapi.responses import FileResponse, StreamingResponse
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, NamedStyle, PatternFill, Side
from sqlalchemy import delete, func, select
from sqlalchemy import and_, delete, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager, joinedload
from sqlalchemy.orm import Session, aliased, contains_eager, joinedload
import brewman.schemas.recipe as schemas
import brewman.schemas.recipe_item as rischemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..models.price import Price
@@ -207,11 +210,44 @@ async def show_list(
user: UserToken = Depends(get_user),
) -> list[schemas.Recipe]:
with SessionFuture() as db:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Recipe.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Recipe.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Recipe.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Recipe.date_,
),
)
list_: Sequence[Recipe] = (
db.execute(
select(Recipe)
.options(joinedload(Recipe.sku, innerjoin=True).joinedload(StockKeepingUnit.product, innerjoin=True))
.order_by(Recipe.sku_id)
.join(Recipe.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.versions)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group)
)
.order_by(ProductVersion.name)
)
.scalars()
.all()
@@ -219,7 +255,7 @@ async def show_list(
return [
schemas.Recipe(
id_=item.id,
sku=schemas.ProductLink(id_=item.sku.id, name=item.sku.product.name),
sku=schemas.ProductLink(id_=item.sku.id, name=item.sku.product.versions[0].name),
date_=item.date_,
source=item.source,
instructions=item.instructions,
@@ -227,8 +263,8 @@ async def show_list(
plating=item.plating,
recipe_yield=item.recipe_yield,
notes=item.notes,
product_group_id=item.sku.product.product_group_id,
units=item.sku.units,
product_group_id=item.sku.product.versions[0].product_group_id,
units=item.sku.versions[0].units,
items=[],
)
for item in list_
@@ -267,7 +303,7 @@ def show_pdf(
@router.get("/xlsx", response_class=StreamingResponse)
def get_report(
p: uuid.UUID | None = None,
p: uuid.UUID,
t: uuid.UUID | None = None,
) -> StreamingResponse:
with SessionFuture() as db:
@@ -281,24 +317,79 @@ def get_report(
.scalars()
.all()
)
prices = [(i.product.name, i.product.fraction_units, i.price) for i in pq]
prices = [(i.product.versions[-1].name, i.product.versions[-1].fraction_units, i.price) for i in pq]
list_: Sequence[Recipe] = []
with SessionFuture() as db:
RecipeProductVersion = aliased(ProductVersion, name="recipe_product_version")
ItemProductVersion = aliased(ProductVersion, name="item_product_version")
CurrentSkuVersion = aliased(SkuVersion, name="current_sku_version")
# The ON clauses for date validity (as in your original)
sku_version_onclause = and_(
CurrentSkuVersion.sku_id == StockKeepingUnit.id,
or_(
CurrentSkuVersion.valid_from == None, # noqa: E711
CurrentSkuVersion.valid_from <= Recipe.date_,
),
or_(
CurrentSkuVersion.valid_till == None, # noqa: E711
CurrentSkuVersion.valid_till >= Recipe.date_,
),
)
recipe_product_version_onclause = and_(
RecipeProductVersion.product_id == StockKeepingUnit.product_id,
or_(
RecipeProductVersion.valid_from == None, # noqa: E711
RecipeProductVersion.valid_from <= Recipe.date_,
),
or_(
RecipeProductVersion.valid_till == None, # noqa: E711
RecipeProductVersion.valid_till >= Recipe.date_,
),
)
item_product_version_onclause = and_(
ItemProductVersion.product_id == RecipeItem.product_id,
or_(
ItemProductVersion.valid_from == None, # noqa: E711
ItemProductVersion.valid_from <= Recipe.date_,
),
or_(
ItemProductVersion.valid_till == None, # noqa: E711
ItemProductVersion.valid_till >= Recipe.date_,
),
)
q = (
select(Recipe)
.join(Recipe.items)
.join(RecipeItem.product)
.join(ItemProductVersion, item_product_version_onclause)
.join(Recipe.sku)
.join(CurrentSkuVersion, sku_version_onclause)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(RecipeProductVersion, recipe_product_version_onclause)
.join(RecipeProductVersion.product_group)
.options(
joinedload(Recipe.items, innerjoin=True).joinedload(RecipeItem.product, innerjoin=True),
contains_eager(Recipe.sku, StockKeepingUnit.product, Product.product_group),
# Eagerly load ingredients and their product & product_version
contains_eager(Recipe.items)
.contains_eager(RecipeItem.product)
.contains_eager(Product.versions, alias=ItemProductVersion),
# Eagerly load sku
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions, alias=RecipeProductVersion)
.contains_eager(RecipeProductVersion.product_group),
# SkuVersion for the SKU (if needed)
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions, alias=CurrentSkuVersion),
)
)
if p is not None:
q = q.where(Recipe.sku, StockKeepingUnit.product, Product.product_group_id == p)
q = q.where(RecipeProductVersion.product_group_id == p)
list_ = db.execute(q).unique().scalars().all()
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.name))
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.versions[0].name))
e.seek(0)
headers = {"Content-Disposition": "attachment; filename = recipe.xlsx"}
@@ -316,21 +407,21 @@ def excel(prices: list[tuple[str, str, Decimal]], recipes: list[Recipe]) -> Byte
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.versions[0].product_group.name for x in recipes])
for pg in pgs:
wb.create_sheet(pg)
rows = defaultdict(lambda: 1)
register_styles(wb)
for recipe in recipes:
ws = wb[recipe.sku.product.product_group.name]
row = rows[recipe.sku.product.product_group.name]
ws = wb[recipe.sku.product.versions[0].product_group.name]
row = rows[recipe.sku.product.versions[0].product_group.name]
print(row)
ings = len(recipe.items)
ing_from = row + 2
ing_till = ing_from + ings - 1
ws.cell(row=row, column=1, value=recipe.sku.product.name).style = "recipe_name"
ws.cell(row=row, column=1, value=recipe.sku.product.versions[0].name).style = "recipe_name"
# ws.cell(row=row, column=3, value=f"Yeild = {recipe.recipe_yield} {recipe.sku.units}")
ws.cell(row=row, column=2, value=recipe.sku.units).style = "recipe_unit"
ws.cell(row=row, column=2, value=recipe.sku.versions[0].units).style = "recipe_unit"
ws.cell(row=row, column=3, value=recipe.recipe_yield).style = "recipe_name"
ws.cell(row=row, column=4).style = "recipe_name"
ws.cell(row=row, column=5, value=f"=SUM(E{ing_from}:E{ing_till})").style = "recipe_name"
@@ -342,12 +433,12 @@ def excel(prices: list[tuple[str, str, Decimal]], recipes: list[Recipe]) -> Byte
ws.cell(row=row, column=5, value="Amount").style = "header"
for item in recipe.items:
row += 1
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=1, value=item.product.versions[0].name).style = "ing"
ws.cell(row=row, column=2, value=item.product.versions[0].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: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
rows[recipe.sku.product.versions[0].product_group.name] = row + 1
virtual_workbook = BytesIO()
wb.save(virtual_workbook)
return virtual_workbook
@@ -466,7 +557,7 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe:
id_=recipe.id,
sku=schemas.ProductLink(
id_=recipe.sku_id,
name=f"{recipe.sku.product.name} ({recipe.sku.units})",
name=f"{recipe.sku.product.versions[0].name} ({recipe.sku.versions[0].units})",
),
date_=recipe.date_,
source=recipe.source,
@@ -480,7 +571,7 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe:
id_=item.id,
product=schemas.ProductLink(
id_=item.product.id,
name=f"{item.product.name} ({item.product.fraction_units})",
name=f"{item.product.versions[0].name} ({item.product.versions[0].fraction_units})",
),
quantity=round(item.quantity, 2),
description=item.description,
@@ -4,12 +4,16 @@ from datetime import date, datetime
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import and_
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import delete, distinct, func, or_, select, update
import brewman.schemas.closing_stock as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_date, set_period
from ...db.session import SessionFuture
@@ -138,23 +142,58 @@ def full_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> schemas.
def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> list[schemas.ClosingStockItem]:
amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount")
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
query = db.execute(
select(StockKeepingUnit, quantity_sum, amount_sum)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(StockKeepingUnit.batches)
.join(Batch.inventories)
.join(Inventory.voucher)
.join(Voucher.journals)
.options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group))
.where(
Voucher.date_ <= date_,
Journal.cost_centre_id == cost_centre_id,
or_(Voucher.voucher_type != VoucherType.CLOSING_STOCK, Voucher.date_ != date_),
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[SkuVersion, Decimal, Decimal]] = (
db.execute(
select(SkuVersion, quantity_sum, amount_sum)
.select_from(Voucher)
.join(Voucher.journals)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group)
)
.where(
Voucher.date_ <= date_,
Journal.cost_centre_id == cost_centre_id,
or_(Voucher.voucher_type != VoucherType.CLOSING_STOCK, Voucher.date_ != date_),
)
.group_by(StockKeepingUnit, Product, ProductVersion, ProductGroup, SkuVersion) # type: ignore
.order_by(ProductGroup.name, ProductVersion.name, SkuVersion.units)
)
.group_by(StockKeepingUnit, Product, ProductGroup) # type: ignore
.order_by(ProductGroup.name, Product.name, StockKeepingUnit.units)
).all()
.unique()
.all()
)
physical_list = (
db.execute(
@@ -179,16 +218,18 @@ def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> list[sc
).all()
body = []
for sku, quantity, amount in query:
for sku_version, quantity, amount in query:
if quantity != 0 and amount != 0:
id_ = next((p.id for p in physical_list if p.sku_id == sku.id), None)
physical = next((p.quantity for p in physical_list if p.sku_id == sku.id), quantity)
cc = next((schemas.CostCentreLink(id_=c) for (c, s) in ccs if s == sku.id), None)
id_ = next((p.id for p in physical_list if p.sku_id == sku_version.sku_id), None)
physical = next((p.quantity for p in physical_list if p.sku_id == sku_version.sku_id), quantity)
cc = next((schemas.CostCentreLink(id_=c) for (c, s) in ccs if s == sku_version.sku_id), None)
body.append(
schemas.ClosingStockItem(
id_=id_,
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
group=sku.product.product_group.name,
product=schemas.ProductLink(
id_=sku_version.sku_id, name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})"
),
group=sku_version.sku.product.versions[0].product_group.name,
quantity=quantity,
amount=amount,
physical=physical,
@@ -289,8 +330,27 @@ def save_cs(data: schemas.ClosingStock, db: Session) -> None:
def save(date_: date, items: list[schemas.ClosingStockItem], user: UserToken, db: Session) -> Voucher:
product_accounts: list[uuid.UUID] = list( # type: ignore
select(Product.account_id).join(Product.skus).where(StockKeepingUnit.id.in_([i.product.id_ for i in items]))
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
product_accounts = (
db.execute(
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product.skus)
.where(StockKeepingUnit.id.in_([i.product.id_ for i in items]))
)
.scalars()
.all()
)
account_types: list[int] = list(
db.execute(select(distinct(AccountBase.type_id)).where(AccountBase.id.in_(product_accounts))).scalars().all()
@@ -4,13 +4,16 @@ from datetime import UTC, date, datetime
from decimal import Decimal
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import desc, func, or_
from sqlalchemy import and_, desc, func, or_
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
import brewman.schemas.mozimo_daily_register as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_date, set_date
from ...db.session import SessionFuture
@@ -42,30 +45,55 @@ def show_data(
body = build_report(d, db)
set_date(date_, request.session)
return schemas.MozimoDailyRegister(
date=d,
date_=d,
body=body,
)
def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterItem]:
body = []
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
products = (
db.execute(
select(StockKeepingUnit)
.join(StockKeepingUnit.product)
.where(Product.product_group_id == uuid.UUID("dad46805-f577-4e5b-8073-9b788e0173fc")) # Menu items
.order_by(Product.name, StockKeepingUnit.units)
select(ProductVersion.name, SkuVersion.units, SkuVersion.sku_id)
.select_from(Product)
.join(ProductVersion, onclause=product_version_onclause)
.join(Product.skus)
.join(SkuVersion, onclause=sku_version_onclause)
.where(ProductVersion.product_group_id == uuid.UUID("dad46805-f577-4e5b-8073-9b788e0173fc")) # Menu items
.order_by(ProductVersion.name, SkuVersion.units)
)
.scalars()
.all()
)
for sku in products:
ob = opening_balance(sku.id, date_, db)
for name, units, sku_id in products:
ob = opening_balance(sku_id, date_, db)
item = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == sku.id,
MozimoStockRegister.sku_id == sku_id,
MozimoStockRegister.date_ == date_,
)
)
@@ -75,7 +103,7 @@ def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterIt
body.append(
schemas.MozimoDailyRegisterItem(
id_=None if item is None else item.id_,
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
product=schemas.ProductLink(id_=sku_id, name=f"{name} ({units})"),
opening=ob,
received=Decimal(0) if item is None else item.received,
sale=Decimal(0) if item is None else item.sale,
@@ -107,14 +135,18 @@ def opening_balance(product_id: uuid.UUID, start_date: date, db: Session) -> Dec
.scalars()
.one_or_none()
)
physical = ((opening_physical.display or 0) + (opening_physical.ageing or 0)) if opening_physical is not None else 0
physical = (
((opening_physical.display or Decimal(0)) + (opening_physical.ageing or Decimal(0)))
if opening_physical is not None
else Decimal(0)
)
query_ = select(func.sum(MozimoStockRegister.received - MozimoStockRegister.sale - MozimoStockRegister.nc)).where(
MozimoStockRegister.sku_id == product_id,
MozimoStockRegister.date_ < start_date,
)
if opening_physical is not None:
query_ = query_.where(MozimoStockRegister.date_ > opening_physical.date_)
calculated = db.execute(query_).scalar() or 0
calculated = db.execute(query_).scalar() or Decimal(0)
return physical + calculated
@@ -53,7 +53,7 @@ def show_data(
return schemas.MozimoProductRegister(
start_date=start_date,
finish_date=finish_date,
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.versions[-1].name} ({sku.versions[-1].units})"),
body=body,
)
@@ -91,7 +91,7 @@ def build_report(
)
if item is not None:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
closing = (item.ageing or Decimal(0)) + (item.display or Decimal(0))
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
@@ -135,7 +135,7 @@ def save_route(
ob = opening_balance(data.product.id_, data.start_date, db)
for item in data.body:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
closing = (item.ageing or Decimal(0)) + (item.display or Decimal(0))
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
@@ -0,0 +1,47 @@
import csv
import io
import uuid
from datetime import date, datetime, time, timedelta
from io import StringIO
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
import brewman.schemas.fingerprint as schemas
from ...core.security import get_user
from ...db.session import SessionFuture
from ...models.employee import Employee
from ...models.fingerprint import Fingerprint
from ...schemas.user import UserToken
router = APIRouter()
def fp(file_data: StringIO, employees: dict[int, uuid.UUID]) -> list[schemas.Fingerprint]:
fingerprints: list[schemas.Fingerprint] = []
reader = csv.reader(file_data, delimiter="\t")
header = next(reader)
employee_column = 2
date_column = len(header) - 1
date_format = "%Y-%m-%d %H:%M:%S"
for row in reader:
try:
employee_code = int(row[employee_column]) # EnNo
date_ = datetime.strptime(row[date_column].replace("/", "-"), date_format)
if employee_code in employees:
fingerprints.append(
schemas.Fingerprint(
id_=uuid.uuid4(),
employee_id=employees[employee_code],
date_=date_,
)
)
except ValueError:
continue
return fingerprints
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Security
from sqlalchemy import select
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import Session, contains_eager, joinedload
import brewman.schemas.non_contract_purchase as schemas
@@ -10,7 +10,9 @@ from brewman.models.batch import Batch
from brewman.models.inventory import Inventory
from brewman.models.journal import Journal
from brewman.models.product import Product
from brewman.models.product_version import ProductVersion
from brewman.models.rate_contract import RateContract
from brewman.models.sku_version import SkuVersion
from brewman.models.stock_keeping_unit import StockKeepingUnit
from brewman.models.voucher import Voucher
from brewman.models.voucher_type import VoucherType
@@ -46,13 +48,41 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
.all()
)
list_: list[schemas.NonContractPurchase] = []
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
for rc in rcs:
for item in rc.items:
invs = db.execute(
select(Voucher.id, Voucher.date_, Inventory.id, Inventory.rate)
.join(Inventory.batch)
.join(Inventory.voucher)
select(Voucher.id, Voucher.date_, Inventory.rate, ProductVersion.name, SkuVersion.units)
.select_from(Voucher)
.join(Voucher.journals)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.where(
Batch.sku_id == item.sku_id,
Voucher.date_ >= rc.valid_from,
@@ -62,24 +92,19 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
Inventory.rate != item.price,
)
).all()
for inv in invs:
p, u = db.execute(
select(Product.name, StockKeepingUnit.units)
.join(Product.skus)
.where(StockKeepingUnit.id == item.sku_id)
).one()
for voucher_id, date_, rate, name, units in invs:
list_.append(
schemas.NonContractPurchase(
date_=inv.date_,
date_=date_,
vendor=rc.vendor.name,
product=f"{p} ({u})",
product=f"{name} ({units})",
url=[
"/",
"purchase",
str(inv.id),
str(voucher_id),
],
contract_price=item.price,
purchase_price=inv.rate,
purchase_price=rate,
)
)
return list_
@@ -4,11 +4,14 @@ from datetime import date, datetime
from decimal import Decimal
from fastapi import APIRouter, Request, Security
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import func, select
import brewman.schemas.product_ledger as schemas
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
@@ -61,7 +64,7 @@ def show_data(
return schemas.ProductLedger(
start_date=start_date,
finish_date=finish_date,
product=schemas.ProductLink(id_=product.id, name=product.name),
product=schemas.ProductLink(id_=product.id, name=product.versions[-1].name),
body=body,
)
@@ -72,16 +75,31 @@ def build_report(
running_total_q, running_total_a, opening = opening_balance(product_id, start_date, db)
body = opening
query = db.execute(
select(Voucher, Inventory, Journal, StockKeepingUnit)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[Voucher, Inventory, Journal, SkuVersion]] = db.execute( # type: ignore[assignment]
select(Voucher, Inventory, Journal, SkuVersion)
.select_from(Voucher)
.join(Voucher.journals)
.join(Journal.account)
.join(Journal.cost_centre)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.options(
joinedload(Journal.account, innerjoin=True),
joinedload(Journal.cost_centre, innerjoin=True),
contains_eager(Voucher.journals).contains_eager(Journal.account),
contains_eager(Voucher.journals).contains_eager(Journal.cost_centre),
)
.where(
StockKeepingUnit.product_id == product_id,
@@ -141,16 +159,30 @@ def build_report(
def opening_balance(
product_id: uuid.UUID, start_date: date, db: Session
) -> tuple[Decimal, Decimal, list[schemas.ProductLedgerItem]]:
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
row = db.execute(
select(
StockKeepingUnit.units,
SkuVersion.units,
func.sum(Inventory.quantity * Journal.debit).label("quantity"),
func.sum(Inventory.amount * Journal.debit).label("amount"),
)
.select_from(Voucher)
.join(Voucher.journals)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(Inventory.voucher)
.join(Voucher.journals)
.join(SkuVersion, onclause=sku_version_onclause)
.where(
Voucher.id == Inventory.voucher_id,
Voucher.id == Journal.voucher_id,
@@ -158,7 +190,7 @@ def opening_balance(
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
Voucher.date_ < start_date,
)
.group_by(StockKeepingUnit.units)
.group_by(SkuVersion.units)
).all()
quantity: Decimal = sum(r.quantity for r in row)
@@ -1,11 +1,14 @@
from datetime import date, datetime
from fastapi import APIRouter, Request, Security
from sqlalchemy import select
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import Session, contains_eager
import brewman.schemas.purchase_entries as schemas
from brewman.models.product import Product
from brewman.models.product_version import ProductVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
@@ -50,6 +53,17 @@ def report_data(
def build_report(start_date: date, finish_date: date, db: Session) -> list[schemas.PurchaseEntriesItem]:
body = []
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
query: list[Voucher] = list(
db.execute(
select(Voucher)
@@ -59,12 +73,14 @@ def build_report(start_date: date, finish_date: date, db: Session) -> list[schem
.join(Inventory.batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.options(
contains_eager(Voucher.journals).contains_eager(Journal.account),
contains_eager(Voucher.inventories)
.contains_eager(Inventory.batch)
.contains_eager(Batch.sku)
.contains_eager(StockKeepingUnit.product),
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
)
.where(
Voucher.date_ >= start_date,
@@ -89,7 +105,7 @@ def build_report(start_date: date, finish_date: date, db: Session) -> list[schem
voucher.voucher_type.name.replace("_", "-").lower(),
str(voucher.id),
],
product=item.batch.sku.product.name,
product=item.batch.sku.product.versions[0].name,
quantity=item.quantity,
rate=item.rate,
tax=item.tax,
+34 -5
View File
@@ -2,12 +2,15 @@ from datetime import datetime
from decimal import Decimal
from fastapi import APIRouter, Request, Security
from sqlalchemy import not_
from sqlalchemy import and_, not_, or_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
import brewman.schemas.purchases as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
@@ -62,25 +65,51 @@ def build_report(
body = []
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount")
query = db.execute(
select(Product, quantity_sum, amount_sum)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[ProductVersion, Decimal, Decimal]] = db.execute(
select(ProductVersion, quantity_sum, amount_sum)
.select_from(Voucher)
.join(Voucher.journals)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= datetime.strptime(start_date, "%d-%b-%Y"),
Voucher.date_ <= datetime.strptime(finish_date, "%d-%b-%Y"),
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
.group_by(Product) # type: ignore
.group_by(ProductVersion) # type: ignore
.order_by(desc(amount_sum))
).all()
total_amount = Decimal(0)
for product, quantity, amount in query:
rate = amount / quantity if quantity != 0 else 0
rate = amount / quantity if quantity != 0 else Decimal(0)
total_amount += amount
row = schemas.PurchasesItem(
name=product.name,
@@ -4,11 +4,15 @@ from datetime import date, datetime
from decimal import Decimal
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import case, func, select
import brewman.schemas.raw_material_cost as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
@@ -89,7 +93,7 @@ def build_report(
sum_issue = func.sum(case((AccountBase.type_id == 2, Journal.signed_amount), else_=0)).label("issue")
sum_sale = func.sum(case((AccountBase.type_id == 3, Journal.signed_amount * -1), else_=0)).label("sale")
query = db.execute(
query: list[tuple[CostCentre, Decimal, Decimal]] = db.execute(
select(CostCentre, sum_issue, sum_sale)
.join(CostCentre.journals)
.join(Journal.voucher)
@@ -109,7 +113,7 @@ def build_report(
for cost_centre, issue, sale in query:
issues += issue
sales += sale
rmc = 0 if sale == 0 else issue / sale
rmc = Decimal(0) if sale == 0 else issue / sale
body.append(
schemas.RawMaterialCostItem(
name=cost_centre.name,
@@ -121,7 +125,7 @@ def build_report(
)
)
rmc = 0 if sales == 0 else issues / sales
rmc = Decimal(0) if sales == 0 else issues / sales
return body, schemas.RawMaterialCostItem(name="Total", issue=issues, sale=sales, rmc=rmc, order=0)
@@ -132,21 +136,46 @@ def build_report_id(
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
sum_gross = func.sum(Inventory.amount * Journal.debit).label("gross")
query = db.execute(
select(Product, ProductGroup, sum_quantity, sum_net, sum_gross)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[ProductVersion, ProductGroup, Decimal, Decimal, Decimal]] = db.execute(
select(ProductVersion, ProductGroup, sum_quantity, sum_net, sum_gross)
.select_from(Voucher)
.join(Voucher.journals)
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(Inventory.voucher)
.join(Voucher.journals)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= start_date,
Voucher.date_ <= finish_date,
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
Journal.cost_centre_id == cost_centre_id,
)
.group_by(Product, ProductGroup, Journal.debit, ProductGroup.name) # type: ignore
.group_by(ProductVersion, Product, ProductGroup, Journal.debit, ProductGroup.name) # type: ignore
.order_by(ProductGroup.name, sum_net.desc())
).all()
+100 -50
View File
@@ -2,12 +2,15 @@ from datetime import date, datetime
from decimal import Decimal
from fastapi import APIRouter, Request, Security
from sqlalchemy import not_
from sqlalchemy import and_, not_, or_
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import func, select
import brewman.schemas.stock_movement as schemas
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
@@ -54,90 +57,137 @@ def report_data(
def build_stock_movement(start_date: date, finish_date: date, db: Session) -> list[schemas.StockMovementItem]:
dict_ = {}
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
openings = db.execute(
select(StockKeepingUnit, quantity_sum)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(StockKeepingUnit.batches)
.join(Batch.inventories)
.join(Inventory.voucher)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
openings: list[tuple[SkuVersion, Decimal]] = db.execute(
select(SkuVersion, quantity_sum)
.select_from(Voucher)
.join(Voucher.journals)
.options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group))
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group)
)
.where(Voucher.date_ < start_date, Journal.cost_centre_id == CostCentre.cost_centre_purchase())
.group_by(StockKeepingUnit, Product, ProductGroup) # type: ignore
.group_by(StockKeepingUnit, Product, ProductVersion, ProductGroup, SkuVersion) # type: ignore
).all()
for sku, quantity in openings:
dict_[sku.id] = schemas.StockMovementItem(
id_=sku.id,
name=f"{sku.product.name} ({sku.units})",
group=sku.product.product_group.name,
for sku_version, quantity in openings:
dict_[sku_version.sku_id] = schemas.StockMovementItem(
id_=sku_version.id,
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
group=sku_version.sku.product.versions[0].product_group.name,
opening=Decimal(round(quantity, 2)),
purchase=Decimal(0),
issue=Decimal(0),
closing=Decimal(0),
url=["/", "product-ledger", str(sku.product.id)],
url=["/", "product-ledger", str(sku_version.sku.product_id)],
)
purchases = db.execute(
select(StockKeepingUnit, quantity_sum)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(StockKeepingUnit.batches)
.join(Batch.inventories)
.join(Inventory.voucher)
purchases: list[tuple[SkuVersion, Decimal]] = db.execute(
select(SkuVersion, quantity_sum)
.select_from(Voucher)
.join(Voucher.journals)
.options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group))
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group)
)
.where(
Voucher.date_ >= start_date,
Voucher.date_ <= finish_date,
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
.group_by(StockKeepingUnit, Product, ProductGroup) # type: ignore
.group_by(StockKeepingUnit, Product, ProductVersion, ProductGroup, SkuVersion) # type: ignore
).all()
for sku, quantity in purchases:
if sku.id in dict_:
dict_[sku.id].purchase = Decimal(round(quantity, 2))
for sku_version, quantity in purchases:
if sku_version.sku_id in dict_:
dict_[sku_version.sku_id].purchase = Decimal(round(quantity, 2))
else:
dict_[sku.id] = schemas.StockMovementItem(
id_=sku.id,
name=f"{sku.product.name} ({sku.units})",
group=sku.product.product_group.name,
dict_[sku_version.sku_id] = schemas.StockMovementItem(
id_=sku_version.id,
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
group=sku_version.sku.product.versions[0].product_group.name,
opening=Decimal(0),
purchase=Decimal(round(quantity, 2)),
issue=Decimal(0),
closing=Decimal(0),
url=["/", "product-ledger", str(sku.product.id)],
url=["/", "product-ledger", str(sku_version.sku.product_id)],
)
issues = db.execute(
select(StockKeepingUnit, quantity_sum)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.join(StockKeepingUnit.batches)
.join(Batch.inventories)
.join(Inventory.voucher)
issues: list[tuple[SkuVersion, Decimal]] = db.execute(
select(SkuVersion, quantity_sum)
.select_from(Voucher)
.join(Voucher.journals)
.options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group))
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group)
)
.where(
Voucher.date_ >= start_date,
Voucher.date_ <= finish_date,
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
.group_by(StockKeepingUnit, Product, ProductGroup) # type: ignore
.group_by(StockKeepingUnit, Product, ProductVersion, ProductGroup, SkuVersion) # type: ignore
).all()
for sku, quantity in issues:
if sku.id in dict_:
dict_[sku.id].issue = Decimal(round(quantity * -1, 2))
for sku_version, quantity in issues:
if sku_version.sku_id in dict_:
dict_[sku_version.sku_id].issue = Decimal(round(quantity * -1, 2))
else:
dict_[sku.id] = schemas.StockMovementItem(
id_=sku.id,
name=f"{sku.product.name} ({sku.units})",
group=sku.product.product_group.name,
dict_[sku_version.sku_id] = schemas.StockMovementItem(
id_=sku_version.id,
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
group=sku_version.sku.product.versions[0].product_group.name,
opening=Decimal(0),
purchase=Decimal(0),
issue=Decimal(round(quantity * -1, 2)),
closing=Decimal(0),
url=["/", "product-ledger", str(sku.product.id)],
url=["/", "product-ledger", str(sku_version.sku.product_id)],
)
list_ = [value for key, value in dict_.items()]
+68 -6
View File
@@ -11,6 +11,9 @@ from sqlalchemy.orm import Session
import brewman.schemas.voucher as output
from brewman.models.product_version import ProductVersion
from brewman.models.sku_version import SkuVersion
from ..core.security import get_current_active_user as get_user
from ..core.session import get_first_day
from ..db.session import SessionFuture
@@ -137,10 +140,26 @@ def delete_voucher(
else:
for i_item in voucher.inventories:
if i_item.batch.quantity_remaining < i_item.quantity:
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == i_item.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"Only {i_item.batch.quantity_remaining}"
f" of {i_item.batch.sku.product.name} remaining.\n"
f" of {product_name} remaining.\n"
f"So it cannot be deleted",
)
i_item.batch.quantity_remaining -= i_item.quantity
@@ -152,9 +171,24 @@ def delete_voucher(
)
).scalar_one()
if uses > 0:
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == i_item.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{i_item.batch.sku.product.name} has been issued and cannot be deleted",
detail=f"{product_name} has been issued and cannot be deleted",
)
batches_to_delete.append(i_item.batch)
elif voucher.voucher_type == VoucherType.PURCHASE_RETURN:
@@ -260,8 +294,38 @@ def voucher_info(voucher: Voucher, db: Session) -> output.Voucher:
if len(json_voucher.incentives) > 0:
json_voucher.incentive = next(x.amount for x in voucher.journals if x.account_id == Account.incentive_id())
for inventory in voucher.inventories:
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == inventory.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
product_unit = db.execute(
select(SkuVersion.units).where(
and_(
SkuVersion.sku_id == inventory.batch.sku_id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
text = (
f"{inventory.batch.sku.product.name} ({inventory.batch.sku.units}) "
f"{product_name} ({product_unit}) "
f"{inventory.batch.quantity_remaining:.2f}@{inventory.batch.rate:.2f} "
f"from {inventory.batch.name.strftime('%d-%b-%Y')}"
)
@@ -282,9 +346,7 @@ def voucher_info(voucher: Voucher, db: Session) -> output.Voucher:
rate=inventory.batch.rate,
sku=output.ProductLink(
id_=inventory.batch.sku.id,
name=f"{inventory.batch.sku.product.name} ({inventory.batch.sku.units})"
if inventory.batch.sku.units
else inventory.batch.sku.product.name,
name=f"{product_name} ({product_unit})" if product_unit else product_name,
),
),
)
@@ -41,7 +41,7 @@ class MozimoDailyRegisterItem(BaseModel):
class MozimoDailyRegister(BaseModel):
date_: date
date_: date | str
body: list[MozimoDailyRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -52,8 +52,8 @@ class MozimoProductRegisterItem(BaseModel):
class MozimoProductRegister(BaseModel):
start_date: date | None = None
finish_date: date | None = None
start_date: date | str
finish_date: date | str
product: ProductLink | None = None
body: list[MozimoProductRegisterItem]
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+1 -1
View File
@@ -46,7 +46,7 @@ class ProductIn(BaseModel):
class Product(ProductIn):
id_: uuid.UUID
code: int
handle: str
is_fixture: bool
@@ -1,5 +1,7 @@
import uuid
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field
from . import Daf, to_camel
@@ -8,8 +10,8 @@ from . import Daf, to_camel
class StockKeepingUnit(BaseModel):
id_: uuid.UUID | None = None
units: str = Field(..., min_length=1)
fraction: Daf = Field(ge=1, default=1)
product_yield: Daf = Field(gt=0, le=1, default=1)
cost_price: Daf = Field(ge=0, default=0)
sale_price: Daf = Field(ge=0, default=0)
fraction: Daf = Field(ge=Decimal(1), default=Decimal(1))
product_yield: Daf = Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))
cost_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
sale_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+23
View File
@@ -0,0 +1,23 @@
"""
Simple script to import all SQLAlchemy models and catch import-time errors.
Place this file in your tests/ directory.
"""
import sys
from pathlib import Path
# Adjust this if your models are in a different location
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
try:
# Adjust the import to match where your models are
from brewman.db import base # e.g., from app import models # noqa: F401
print("✅ All models imported successfully.")
except Exception as e:
print("❌ Error importing models!")
print(e)
sys.exit(1)
+2 -15
View File
@@ -12,25 +12,12 @@ else
./version_bump.sh
fi
# Download the package.json for caching
curl --silent 'https://git.tanshu.com/tanshu/brewman/raw/tag/latest/overlord/package.json' \
| sed 's/\"version\": \"[0-9\.]*\"/"version": "0.0.0"/g' \
> "$parent_path/docker/app/package.json"
# Download the package.json for caching
curl --silent 'https://git.tanshu.com/tanshu/brewman/raw/tag/latest/brewman/pyproject.toml' \
| sed 's/version = \"[0-9\.]*\"/version = "0.0.0"/g' \
> "$parent_path/docker/app/pyproject.toml"
cd "$parent_path/docker/app" || exit
docker build --tag brewman:latest .
make build-production
if [ 1 -eq "$#" ]
then
docker tag brewman:latest "$1"
else
echo "No version bump"
fi
cd "$parent_path/docker" || exit
docker save brewman:latest | bzip2 | pv | ssh gondor 'bunzip2 | sudo docker load'
cd "$parent_path/ansible" || exit
ansible-playbook --inventory hosts playbook.yml
@@ -1,11 +1,7 @@
import { CdkScrollable } from '@angular/cdk/scrolling';
import { AsyncPipe, CurrencyPipe } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
import { MatButton } from '@angular/material/button';
import { MatOption } from '@angular/material/core';
import { MatDatepicker } from '@angular/material/datepicker';
import {
MAT_DIALOG_DATA,
MatDialogRef,
@@ -14,12 +10,10 @@ import {
MatDialogActions,
MatDialogClose,
} from '@angular/material/dialog';
import { MatFormField, MatLabel, MatHint, MatPrefix } from '@angular/material/form-field';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { MatSelect } from '@angular/material/select';
import { Tag } from '../tag/tag';
import { AccountingPipe } from '../shared/accounting.pipe';
import { MatCheckbox } from '@angular/material/checkbox';
import { LedgerItem } from '../ledger/ledger-item';
import { TagList } from './tag-list';
@@ -32,24 +26,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
styleUrls: ['./tag-dialog.component.css'],
standalone: true,
imports: [
AccountingPipe,
AsyncPipe,
CurrencyPipe,
CdkScrollable,
MatCheckbox,
MatDialogTitle,
MatDialogContent,
ReactiveFormsModule,
MatFormField,
MatSelect,
MatDatepicker,
MatOption,
MatLabel,
MatInput,
MatAutocompleteTrigger,
MatHint,
MatAutocomplete,
MatPrefix,
MatDialogActions,
MatButton,
MatDialogClose,