Changed the build system. Now I am targeting multi arch. Added Product Version and SkuVersion
This commit is contained in:
1 parent
0d2dea4eff
commit
05b395af00
48 files changed
+2379
-561
No files matched your search
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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})",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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_
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user