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
|
HOST=0.0.0.0
|
||||||
PORT=80
|
PORT=9998
|
||||||
LOG_LEVEL=WARN
|
LOG_LEVEL=WARNING
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
SQLALCHEMY_DATABASE_URI=
|
SQLALCHEMY_DATABASE_URI=postgresql+psycopg://postgres:123456@10.1.1.1:5432/hops
|
||||||
MODULE_NAME=brewman.main
|
MODULE_NAME=brewman.main
|
||||||
PROJECT_NAME=brewman
|
PROJECT_NAME=brewman
|
||||||
POSTGRES_SERVER=
|
POSTGRES_SERVER=localhost
|
||||||
POSTGRES_USER=
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=123456
|
||||||
POSTGRES_DB=
|
POSTGRES_DB=hops
|
||||||
|
|
||||||
# openssl rand -hex 32
|
PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAX5qCMOidCSToKo2oe6eNii+Jg9qKiQDz2+JD/r3gQAs=\n-----END PUBLIC KEY-----
|
||||||
SECRET_KEY=
|
PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIHlhlEP7acCnOWq6VUWJN/q2NU2Mwo7eEtMo1em6luy7\n-----END PRIVATE KEY-----
|
||||||
# openssl rand -hex 5
|
# openssl rand -hex 5
|
||||||
MIDDLEWARE_SECRET_KEY=
|
MIDDLEWARE_SECRET_KEY=c982367648
|
||||||
ALGORITHM=HS256
|
ALGORITHM=EdDSA
|
||||||
JWT_TOKEN_EXPIRE_MINUTES=30
|
JWT_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
|
||||||
ALEMBIC_LOG_LEVEL=INFO
|
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
|
```sh
|
||||||
poetry install
|
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
|
import secrets
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
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
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# openssl rand -hex 32
|
# openssl rand -hex 32
|
||||||
TITLE: str = ""
|
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)
|
MIDDLEWARE_SECRET_KEY: str = secrets.token_urlsafe(5)
|
||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
JWT_TOKEN_EXPIRE_MINUTES: int = 30
|
JWT_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
@@ -22,6 +24,18 @@ class Settings(BaseSettings):
|
|||||||
POSTGRES_DB: str = ""
|
POSTGRES_DB: str = ""
|
||||||
SQLALCHEMY_DATABASE_URI: str | None = None
|
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")
|
@model_validator(mode="after")
|
||||||
def assemble_db_connection(self) -> "Settings":
|
def assemble_db_connection(self) -> "Settings":
|
||||||
if isinstance(self.SQLALCHEMY_DATABASE_URI, str):
|
if isinstance(self.SQLALCHEMY_DATABASE_URI, str):
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import uuid
|
|||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Security, status
|
from fastapi import Depends, HTTPException, Security, status
|
||||||
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
||||||
from jose import jwt
|
|
||||||
from jose.exceptions import ExpiredSignatureError
|
|
||||||
from jwt import PyJWTError
|
from jwt import PyJWTError
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -37,7 +37,7 @@ def create_access_token(*, data: dict[str, Any], expires_delta: timedelta | None
|
|||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
expire = datetime.now(UTC).replace(tzinfo=None) + (expires_delta or timedelta(minutes=15))
|
expire = datetime.now(UTC).replace(tzinfo=None) + (expires_delta or timedelta(minutes=15))
|
||||||
to_encode.update({"exp": expire})
|
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
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
@@ -80,13 +80,13 @@ async def get_current_user(
|
|||||||
headers={"WWW-Authenticate": authenticate_value},
|
headers={"WWW-Authenticate": authenticate_value},
|
||||||
)
|
)
|
||||||
try:
|
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 ""
|
username: str = payload.get("sub") or ""
|
||||||
if username is None:
|
if username is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
token_scopes = payload.get("scopes", [])
|
token_scopes = payload.get("scopes", [])
|
||||||
token_data = TokenData(scopes=token_scopes, username=username)
|
token_data = TokenData(scopes=token_scopes, username=username)
|
||||||
except (PyJWTError, ValidationError, ExpiredSignatureError):
|
except (PyJWTError, ValidationError):
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
user = get_user(
|
user = get_user(
|
||||||
username=token_data.username,
|
username=token_data.username,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from ..models.permission import Permission # noqa: F401
|
|||||||
from ..models.price import Price # noqa: F401
|
from ..models.price import Price # noqa: F401
|
||||||
from ..models.product import Product # noqa: F401
|
from ..models.product import Product # noqa: F401
|
||||||
from ..models.product_group import ProductGroup # noqa: F401
|
from ..models.product_group import ProductGroup # noqa: F401
|
||||||
|
from ..models.product_version import ProductVersion # noqa: F401
|
||||||
from ..models.rate_contract import RateContract # noqa: F401
|
from ..models.rate_contract import RateContract # noqa: F401
|
||||||
from ..models.rate_contract_item import RateContractItem # noqa: F401
|
from ..models.rate_contract_item import RateContractItem # noqa: F401
|
||||||
from ..models.recipe import Recipe # noqa: F401
|
from ..models.recipe import Recipe # noqa: F401
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy.orm import Mapped, relationship
|
|
||||||
|
|
||||||
from ..db.base_class import reg
|
from ..db.base_class import reg
|
||||||
from .account_base import AccountBase
|
from .account_base import AccountBase
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .product import Product
|
|
||||||
|
|
||||||
|
|
||||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||||
class Account(AccountBase):
|
class Account(AccountBase):
|
||||||
__mapper_args__ = {"polymorphic_identity": ""} # type: ignore
|
__mapper_args__ = {"polymorphic_identity": ""} # type: ignore
|
||||||
|
|
||||||
products: Mapped[list["Product"]] = relationship(
|
|
||||||
"Product", primaryjoin="Account.id==Product.account_id", back_populates="account"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from .account_type import AccountType
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .cost_centre import CostCentre
|
from .cost_centre import CostCentre
|
||||||
from .journal import Journal
|
from .journal import Journal
|
||||||
from .product import Product
|
from .product_version import ProductVersion
|
||||||
from .rate_contract import RateContract
|
from .rate_contract import RateContract
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class AccountBase:
|
|||||||
|
|
||||||
journals: Mapped[list["Journal"]] = relationship("Journal", back_populates="account")
|
journals: Mapped[list["Journal"]] = relationship("Journal", back_populates="account")
|
||||||
cost_centre: Mapped["CostCentre"] = relationship("CostCentre", back_populates="accounts")
|
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")
|
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)
|
sale: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||||
nc: 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
|
# 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
|
# 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)
|
last_edit_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
|
||||||
|
|
||||||
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit")
|
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
date_: datetime.date,
|
date_: date,
|
||||||
received: Decimal,
|
received: Decimal,
|
||||||
sale: Decimal,
|
sale: Decimal,
|
||||||
nc: Decimal,
|
nc: Decimal,
|
||||||
display: Decimal,
|
display: Decimal | None = None,
|
||||||
ageing: Decimal,
|
ageing: Decimal | None = None,
|
||||||
sku_id: uuid.UUID | None = None,
|
sku_id: uuid.UUID | None = None,
|
||||||
sku: StockKeepingUnit | None = None,
|
sku: StockKeepingUnit | None = None,
|
||||||
id_: uuid.UUID | None = None,
|
id_: uuid.UUID | None = None,
|
||||||
|
|||||||
@@ -1,119 +1,27 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import TYPE_CHECKING
|
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 sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from ..db.base_class import reg
|
from ..db.base_class import reg
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .account import Account
|
from .product_version import ProductVersion
|
||||||
from .product_group import ProductGroup
|
|
||||||
from .stock_keeping_unit import StockKeepingUnit
|
from .stock_keeping_unit import StockKeepingUnit
|
||||||
|
|
||||||
|
|
||||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||||
class Product:
|
class Product:
|
||||||
__tablename__ = "products"
|
__tablename__ = "products"
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||||
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)
|
|
||||||
is_fixture: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
is_fixture: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||||
is_purchased: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
versions: Mapped[list["ProductVersion"]] = relationship(back_populates="product")
|
||||||
is_sold: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
|
||||||
|
|
||||||
skus: Mapped[list["StockKeepingUnit"]] = relationship("StockKeepingUnit", 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:
|
if id_ is not None:
|
||||||
self.id = id_
|
self.id = id_
|
||||||
if is_fixture is not None:
|
self.is_fixture = is_fixture if is_fixture is not None else False
|
||||||
self.is_fixture = is_fixture
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suspense(cls) -> uuid.UUID:
|
|
||||||
return uuid.UUID("aa79a643-9ddc-4790-ac7f-a41f9efb4c15")
|
|
||||||
@@ -9,7 +9,7 @@ from ..db.base_class import reg
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .product import Product
|
from .product_version import ProductVersion
|
||||||
|
|
||||||
|
|
||||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||||
@@ -22,7 +22,7 @@ class ProductGroup:
|
|||||||
ice_cream: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
ice_cream: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||||
is_fixture: 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__(
|
def __init__(
|
||||||
self,
|
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
|
import uuid
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from typing import TYPE_CHECKING
|
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 sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from ..db.base_class import reg
|
from ..db.base_class import reg
|
||||||
@@ -15,43 +14,29 @@ if TYPE_CHECKING:
|
|||||||
from .batch import Batch
|
from .batch import Batch
|
||||||
from .product import Product
|
from .product import Product
|
||||||
from .recipe import Recipe
|
from .recipe import Recipe
|
||||||
|
from .sku_version import SkuVersion
|
||||||
|
|
||||||
|
|
||||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||||
class StockKeepingUnit:
|
class StockKeepingUnit:
|
||||||
__tablename__ = "stock_keeping_units"
|
__tablename__ = "stock_keeping_units"
|
||||||
__table_args__ = (UniqueConstraint("product_id", "units"),)
|
|
||||||
|
|
||||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, insert_default=uuid.uuid4)
|
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)
|
product_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("products.id"), unique=True, 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: Mapped[Product] = relationship("Product", back_populates="skus")
|
product: Mapped[Product] = relationship("Product", back_populates="skus")
|
||||||
batches: Mapped[list[Batch]] = relationship("Batch", back_populates="sku")
|
batches: Mapped[list[Batch]] = relationship("Batch", back_populates="sku")
|
||||||
recipes: Mapped[list[Recipe]] = relationship("Recipe", back_populates="sku")
|
recipes: Mapped[list[Recipe]] = relationship("Recipe", back_populates="sku")
|
||||||
|
versions: Mapped[list[SkuVersion]] = relationship(back_populates="sku")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
units: str,
|
|
||||||
fraction: Decimal,
|
|
||||||
product_yield: Decimal,
|
|
||||||
cost_price: Decimal,
|
|
||||||
sale_price: Decimal,
|
|
||||||
product_id: uuid.UUID | None = None,
|
product_id: uuid.UUID | None = None,
|
||||||
product: Product | None = None,
|
product: Product | None = None,
|
||||||
id_: uuid.UUID | None = None,
|
id_: uuid.UUID | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if product_id is not None:
|
if product_id is not None:
|
||||||
self.product_id = product_id
|
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:
|
if id_ is not None:
|
||||||
self.id = id_
|
self.id = id_
|
||||||
if product is not None:
|
if product is not None:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import date, timedelta
|
from datetime import UTC, date, datetime, timedelta
|
||||||
|
|
||||||
from sqlalchemy import or_, select
|
from sqlalchemy import or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -73,3 +73,13 @@ def get_lock_info(
|
|||||||
|
|
||||||
return True, "Voucher allowed"
|
return True, "Voucher allowed"
|
||||||
return False, "Default Fallthrough"
|
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
|
import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy import select
|
from sqlalchemy import and_, or_, select
|
||||||
from sqlalchemy.orm import contains_eager
|
from sqlalchemy.orm import contains_eager
|
||||||
|
|
||||||
import brewman.schemas.batch as schemas
|
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 ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
from ..models.batch import Batch
|
from ..models.batch import Batch
|
||||||
@@ -26,21 +29,50 @@ def batch_term(
|
|||||||
date_ = datetime.datetime.strptime(d, "%d-%b-%Y")
|
date_ = datetime.datetime.strptime(d, "%d-%b-%Y")
|
||||||
list_: list[schemas.Batch] = []
|
list_: list[schemas.Batch] = []
|
||||||
with SessionFuture() as db:
|
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 = (
|
query = (
|
||||||
select(Batch)
|
select(Batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.where(Batch.quantity_remaining > 0, Batch.name <= date_)
|
.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:
|
if q is not None:
|
||||||
for q_item in q.split():
|
for q_item in q.split():
|
||||||
query = query.where(Product.name.ilike(f"%{q_item}%"))
|
query = query.where(ProductVersion.name.ilike(f"%{q_item}%"))
|
||||||
result = db.execute(query.order_by(Product.name).order_by(Batch.name)).scalars().all()
|
result = db.execute(query.order_by(ProductVersion.name).order_by(Batch.name)).unique().scalars().all()
|
||||||
|
|
||||||
for item in result:
|
for item in result:
|
||||||
|
product_name = item.sku.product.versions[0].name
|
||||||
|
product_units = item.sku.versions[0].units
|
||||||
text = (
|
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')}"
|
f"{item.rate:.2f} from {item.name.strftime('%d-%b-%Y')}"
|
||||||
)
|
)
|
||||||
list_.append(
|
list_.append(
|
||||||
@@ -53,7 +85,7 @@ def batch_term(
|
|||||||
discount=round(item.discount, 5),
|
discount=round(item.discount, 5),
|
||||||
sku=schemas.ProductLink(
|
sku=schemas.ProductLink(
|
||||||
id_=item.sku.id,
|
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 decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Security
|
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
|
from sqlalchemy.orm import Session, contains_eager
|
||||||
|
|
||||||
import brewman.schemas.batch_integrity as schemas
|
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 ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
from ..models.batch import Batch
|
from ..models.batch import Batch
|
||||||
@@ -37,17 +39,29 @@ def post_check_batch_integrity(
|
|||||||
|
|
||||||
def negative_batches(db: Session) -> list[schemas.BatchIntegrity]:
|
def negative_batches(db: Session) -> list[schemas.BatchIntegrity]:
|
||||||
inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity")
|
inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity")
|
||||||
list_ = db.execute(
|
product_version_onclause = and_(
|
||||||
select(Batch, Product.name, inv_sum)
|
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(Batch.sku)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
.join(Batch.inventories)
|
.join(Batch.inventories)
|
||||||
.join(Inventory.voucher)
|
.join(Inventory.voucher)
|
||||||
.join(Voucher.journals)
|
.join(Voucher.journals)
|
||||||
.where(
|
.where(
|
||||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
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)
|
.having(Batch.quantity_remaining != inv_sum)
|
||||||
).all()
|
).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]:
|
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_ = (
|
list_ = (
|
||||||
db.execute(
|
db.execute(
|
||||||
select(Batch)
|
select(Batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
.join(Batch.inventories)
|
.join(Batch.inventories)
|
||||||
.join(Inventory.voucher)
|
.join(Inventory.voucher)
|
||||||
.where(Voucher.date_ < Batch.name)
|
.where(Voucher.date_ < Batch.name)
|
||||||
@@ -116,7 +142,7 @@ def batch_dates(db: Session) -> list[schemas.BatchIntegrity]:
|
|||||||
issue.append(
|
issue.append(
|
||||||
schemas.BatchIntegrity(
|
schemas.BatchIntegrity(
|
||||||
id_=batch.id,
|
id_=batch.id,
|
||||||
product=batch.sku.product.name,
|
product=batch.sku.product.versions[0].name,
|
||||||
date_=batch.name,
|
date_=batch.name,
|
||||||
showing=batch.quantity_remaining,
|
showing=batch.quantity_remaining,
|
||||||
actual=Decimal(0),
|
actual=Decimal(0),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import uuid
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
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.dialects.postgresql import insert as pg_insert
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -14,6 +14,7 @@ from brewman.models.inventory import Inventory
|
|||||||
from brewman.models.journal import Journal
|
from brewman.models.journal import Journal
|
||||||
from brewman.models.period import Period
|
from brewman.models.period import Period
|
||||||
from brewman.models.price import Price
|
from brewman.models.price import Price
|
||||||
|
from brewman.models.sku_version import SkuVersion
|
||||||
from brewman.models.voucher import Voucher
|
from brewman.models.voucher import Voucher
|
||||||
from brewman.models.voucher_type import VoucherType
|
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]:
|
def get_issue_prices(period: Period, products: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
|
||||||
sum_quantity = func.sum(
|
sum_quantity = func.sum(Inventory.quantity * SkuVersion.fraction * SkuVersion.product_yield * Journal.debit).label(
|
||||||
Inventory.quantity * StockKeepingUnit.fraction * StockKeepingUnit.product_yield * Journal.debit
|
"quantity"
|
||||||
).label("quantity")
|
)
|
||||||
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
|
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] = {}
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
query = db.execute(
|
query = db.execute(
|
||||||
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
|
select(StockKeepingUnit.product_id, issue_price)
|
||||||
|
.select_from(Inventory)
|
||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(Inventory.voucher)
|
.join(Inventory.voucher)
|
||||||
.join(Voucher.journals)
|
.join(Voucher.journals)
|
||||||
.where(
|
.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]:
|
def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
|
||||||
sum_quantity = func.sum(
|
sum_quantity = func.sum(Inventory.quantity * SkuVersion.fraction * SkuVersion.product_yield * Journal.debit).label(
|
||||||
Inventory.quantity * StockKeepingUnit.fraction * StockKeepingUnit.product_yield * Journal.debit
|
"quantity"
|
||||||
).label("quantity")
|
)
|
||||||
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
|
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] = {}
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
|
|
||||||
query = db.execute(
|
query = db.execute(
|
||||||
select(StockKeepingUnit.product_id, sum_net / sum_quantity)
|
select(StockKeepingUnit.product_id, purchase_price)
|
||||||
|
.select_from(Inventory)
|
||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(Inventory.voucher)
|
.join(Inventory.voucher)
|
||||||
.join(Voucher.journals)
|
.join(Voucher.journals)
|
||||||
.where(
|
.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]:
|
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] = {}
|
d: dict[uuid.UUID, Decimal] = {}
|
||||||
query = db.execute(
|
query = db.execute(
|
||||||
select(
|
select(
|
||||||
StockKeepingUnit.product_id,
|
StockKeepingUnit.product_id,
|
||||||
StockKeepingUnit.cost_price / (StockKeepingUnit.fraction * StockKeepingUnit.product_yield),
|
rest_price,
|
||||||
).where(StockKeepingUnit.product_id.in_(req))
|
)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
|
.where(StockKeepingUnit.product_id.in_(req))
|
||||||
).all()
|
).all()
|
||||||
for id, amount in query:
|
for id, amount in query:
|
||||||
d[id] = amount
|
d[id] = amount
|
||||||
@@ -127,6 +169,8 @@ def calculate_recipes(recipes: set[uuid.UUID], prices: dict[uuid.UUID, Decimal],
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for item in items:
|
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
|
prices[item.sku.product_id] = cost
|
||||||
recipes.remove(item.sku.product_id)
|
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()
|
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
|
start_date = employee.joining_date if employee.joining_date > start_date else start_date
|
||||||
finish_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(
|
info = schemas.EmployeeAttendance(
|
||||||
start_date=start_date,
|
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]:
|
def get_prints(employee_id: uuid.UUID, date_: date, db: Session) -> tuple[str, str, bool]:
|
||||||
prints = (
|
prints = list(
|
||||||
db.execute(
|
db.execute(
|
||||||
select(Fingerprint)
|
select(Fingerprint)
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ from datetime import UTC, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
|
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.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.input as schema_in
|
||||||
import brewman.schemas.voucher as output
|
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.security import get_current_active_user as get_user
|
||||||
from ..core.session import get_date, set_date
|
from ..core.session import get_date, set_date
|
||||||
from ..db.session import SessionFuture
|
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]:
|
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 = (
|
product_accounts = (
|
||||||
select(Product.account_id)
|
select(ProductVersion.account_id)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.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]))
|
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
|
||||||
)
|
)
|
||||||
account_types: list[int] = list(
|
account_types: list[int] = list(
|
||||||
@@ -116,16 +135,45 @@ def save_inventories(
|
|||||||
amount: Decimal = Decimal(0)
|
amount: Decimal = Decimal(0)
|
||||||
for data_item in inventories:
|
for data_item in inventories:
|
||||||
batch = db.execute(select(Batch).where(Batch.id == data_item.batch.id_)).scalar_one()
|
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:
|
if batch_consumed and data_item.quantity > batch.quantity_remaining:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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_:
|
if batch.name > voucher.date_:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail=f"{batch.sku.product.name} ({batch.sku.units}) "
|
detail=f"{product_name} ({product_unit}) was purchased on {batch.name.strftime('%d-%b-%Y')}",
|
||||||
f"was purchased on {batch.name.strftime('%d-%b-%Y')}",
|
|
||||||
)
|
)
|
||||||
if batch_consumed is None:
|
if batch_consumed is None:
|
||||||
pass
|
pass
|
||||||
@@ -205,9 +253,25 @@ def update_voucher(
|
|||||||
id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session
|
id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session
|
||||||
) -> tuple[Voucher, bool | None]:
|
) -> tuple[Voucher, bool | None]:
|
||||||
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
|
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 = (
|
product_accounts = (
|
||||||
select(Product.account_id)
|
select(ProductVersion.account_id)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.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]))
|
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
|
||||||
)
|
)
|
||||||
account_types: list[int] = list(
|
account_types: list[int] = list(
|
||||||
@@ -280,18 +344,47 @@ def update_inventories(
|
|||||||
index = next(
|
index = next(
|
||||||
(idx for (idx, d) in enumerate(inventories) if d.id_ == item.id and d.batch.id_ == item.batch.id), None
|
(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:
|
if index is not None:
|
||||||
new_inventory = inventories.pop(index)
|
new_inventory = inventories.pop(index)
|
||||||
if batch_consumed and new_inventory.quantity > batch_quantity:
|
if batch_consumed and new_inventory.quantity > batch_quantity:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail=f"Maximum quantity available for "
|
detail=f"Maximum quantity available for {product_name} ({product_unit}) is {batch_quantity}",
|
||||||
f"{batch.sku.product.name} ({batch.sku.units}) is {batch_quantity}",
|
|
||||||
)
|
)
|
||||||
if item.batch.name > voucher.date_:
|
if item.batch.name > voucher.date_:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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:
|
if batch_consumed is None:
|
||||||
@@ -315,7 +408,7 @@ def update_inventories(
|
|||||||
if batch_quantity < item.quantity:
|
if batch_quantity < item.quantity:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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}",
|
f" minimum quantity is {batch_quantity}",
|
||||||
)
|
)
|
||||||
item.batch.quantity_remaining = batch_quantity
|
item.batch.quantity_remaining = batch_quantity
|
||||||
|
|||||||
+391
-127
@@ -1,25 +1,30 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from datetime import datetime
|
from collections.abc import Sequence
|
||||||
|
from datetime import date, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Security, status
|
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.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session, contains_eager
|
from sqlalchemy.orm import Session, contains_eager
|
||||||
|
|
||||||
import brewman.schemas.product as schemas
|
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 ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
from ..models.account import Account
|
from ..models.account import Account
|
||||||
from ..models.batch import Batch
|
|
||||||
from ..models.product import Product
|
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 import RateContract
|
||||||
from ..models.rate_contract_item import RateContractItem
|
from ..models.rate_contract_item import RateContractItem
|
||||||
from ..models.stock_keeping_unit import StockKeepingUnit
|
from ..models.stock_keeping_unit import StockKeepingUnit
|
||||||
from ..schemas.product_sku import ProductSku
|
from ..schemas.product_sku import ProductSku
|
||||||
from ..schemas.user import UserToken
|
from ..schemas.user import UserToken
|
||||||
|
from . import effective_date
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -32,13 +37,17 @@ def save(
|
|||||||
) -> schemas.Product:
|
) -> schemas.Product:
|
||||||
try:
|
try:
|
||||||
with SessionFuture() as db:
|
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,
|
name=data.name,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
fraction_units=data.fraction_units,
|
fraction_units=data.fraction_units,
|
||||||
product_group_id=data.product_group.id_,
|
product_group_id=data.product_group.id_,
|
||||||
account_id=Account.all_purchases(),
|
account_id=Account.all_purchases(),
|
||||||
is_active=data.is_active,
|
|
||||||
is_purchased=data.is_purchased,
|
is_purchased=data.is_purchased,
|
||||||
is_sold=data.is_sold,
|
is_sold=data.is_sold,
|
||||||
allergen=data.allergen,
|
allergen=data.allergen,
|
||||||
@@ -55,27 +64,34 @@ def save(
|
|||||||
other_solids=data.other_solids,
|
other_solids=data.other_solids,
|
||||||
total_solids=data.total_solids,
|
total_solids=data.total_solids,
|
||||||
water=data.water,
|
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(version)
|
||||||
db.add(item)
|
|
||||||
if not len(data.skus):
|
if not len(data.skus):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail="Not enough stock keeping units.",
|
detail="Not enough stock keeping units.",
|
||||||
)
|
)
|
||||||
for sku in data.skus:
|
for sku in data.skus:
|
||||||
db.add(
|
s = StockKeepingUnit(product=product)
|
||||||
StockKeepingUnit(
|
product.skus.append(s)
|
||||||
units=sku.units,
|
db.add(s)
|
||||||
fraction=round(sku.fraction, 5),
|
db.flush() # So sku.id is available
|
||||||
product_yield=round(sku.product_yield, 5),
|
sver = SkuVersion(
|
||||||
cost_price=round(sku.cost_price, 2),
|
units=sku.units,
|
||||||
sale_price=round(sku.sale_price, 2),
|
fraction=round(sku.fraction, 5),
|
||||||
product=item,
|
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()
|
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:
|
except SQLAlchemyError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -87,77 +103,184 @@ def save(
|
|||||||
def update_route(
|
def update_route(
|
||||||
id_: uuid.UUID,
|
id_: uuid.UUID,
|
||||||
data: schemas.ProductIn,
|
data: schemas.ProductIn,
|
||||||
|
date_: date = Depends(effective_date),
|
||||||
user: UserToken = Security(get_user, scopes=["products"]),
|
user: UserToken = Security(get_user, scopes=["products"]),
|
||||||
) -> schemas.Product:
|
) -> schemas.Product:
|
||||||
try:
|
try:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
|
version: ProductVersion = db.execute(
|
||||||
if item.is_fixture:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_423_LOCKED,
|
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):
|
if not len(data.skus):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail="Not enough stock keeping units.",
|
detail="Not enough stock keeping units.",
|
||||||
)
|
)
|
||||||
for i in range(len(item.skus), 0, -1):
|
old_skus = (
|
||||||
sku = item.skus[i - 1]
|
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)
|
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None)
|
||||||
if index is not None:
|
if index is not None:
|
||||||
new_data_sku = data.skus.pop(index)
|
new_data_sku = data.skus.pop(index)
|
||||||
sku.units = new_data_sku.units
|
sku_changed = (
|
||||||
sku.fraction = round(new_data_sku.fraction, 5)
|
new_data_sku.units != sku.units
|
||||||
sku.product_yield = round(new_data_sku.product_yield, 5)
|
or Decimal(new_data_sku.fraction).quantize(Decimal("0.00001")) != sku.fraction
|
||||||
sku.cost_price = round(new_data_sku.cost_price, 2)
|
or Decimal(new_data_sku.product_yield).quantize(Decimal("0.00001")) != sku.product_yield
|
||||||
sku.sale_price = round(new_data_sku.sale_price, 2)
|
or Decimal(new_data_sku.cost_price).quantize(Decimal("0.01")) != sku.cost_price
|
||||||
else:
|
or Decimal(new_data_sku.sale_price).quantize(Decimal("0.01")) != sku.sale_price
|
||||||
count: int = db.execute(select(func.count()).where(Batch.sku_id == sku.id)).scalar_one()
|
)
|
||||||
if count > 0:
|
|
||||||
raise HTTPException(
|
print(f"SKU Changed: {sku_changed} for {sku.id}")
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
if sku_changed:
|
||||||
detail=f"SKU '{sku.units}' has entries and cannot be deleted.",
|
new_sku = (
|
||||||
|
sku
|
||||||
|
if sku.valid_from == date_
|
||||||
|
else SkuVersion(
|
||||||
|
sku_id=sku.sku_id,
|
||||||
|
valid_from=date_,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
item.skus.remove(sku)
|
new_sku.units = new_data_sku.units
|
||||||
db.delete(sku)
|
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:
|
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,
|
units=data_sku.units,
|
||||||
fraction=round(data_sku.fraction, 5),
|
fraction=round(data_sku.fraction, 5),
|
||||||
product_yield=round(data_sku.product_yield, 5),
|
product_yield=round(data_sku.product_yield, 5),
|
||||||
cost_price=round(data_sku.cost_price, 2),
|
cost_price=round(data_sku.cost_price, 2),
|
||||||
sale_price=round(data_sku.sale_price, 2),
|
sale_price=round(data_sku.sale_price, 2),
|
||||||
product=item,
|
sku=new_s,
|
||||||
)
|
)
|
||||||
db.add(new_sku)
|
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()
|
db.commit()
|
||||||
return product_info(item)
|
return product_info(version, current_skus)
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -168,6 +291,7 @@ def update_route(
|
|||||||
@router.delete("/{id_}", response_model=schemas.ProductBlank)
|
@router.delete("/{id_}", response_model=schemas.ProductBlank)
|
||||||
def delete_route(
|
def delete_route(
|
||||||
id_: uuid.UUID,
|
id_: uuid.UUID,
|
||||||
|
date_: date = Depends(effective_date),
|
||||||
user: UserToken = Security(get_user, scopes=["products"]),
|
user: UserToken = Security(get_user, scopes=["products"]),
|
||||||
) -> schemas.ProductBlank:
|
) -> schemas.ProductBlank:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
@@ -175,17 +299,49 @@ def delete_route(
|
|||||||
if item.is_fixture:
|
if item.is_fixture:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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()
|
).scalar_one()
|
||||||
if count > 0:
|
skus = (
|
||||||
raise HTTPException(
|
db.execute(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Product has entries and cannot be deleted."
|
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_))
|
.scalars()
|
||||||
db.execute(delete(Product).where(Product.id == id_))
|
.all()
|
||||||
|
)
|
||||||
|
valid_till = date_ - timedelta(days=1)
|
||||||
|
version.valid_till = valid_till
|
||||||
|
for sku in skus:
|
||||||
|
sku.valid_till = valid_till
|
||||||
db.commit()
|
db.commit()
|
||||||
return product_blank()
|
return product_blank()
|
||||||
|
|
||||||
@@ -198,20 +354,45 @@ def show_blank(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[schemas.Product])
|
@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:
|
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 [
|
return [
|
||||||
product_info(item)
|
product_info(item, [s.versions[0] for s in item.product.skus])
|
||||||
for item in db.execute(
|
for item in db.execute(
|
||||||
select(Product)
|
select(ProductVersion)
|
||||||
.join(Product.product_group)
|
.join(ProductVersion.product)
|
||||||
|
.join(ProductVersion.product_group)
|
||||||
|
.join(ProductVersion.product)
|
||||||
.join(Product.skus)
|
.join(Product.skus)
|
||||||
.order_by(desc(Product.is_active))
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.order_by(Product.product_group_id)
|
.where(
|
||||||
.order_by(Product.name)
|
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(
|
.options(
|
||||||
contains_eager(Product.skus),
|
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
|
||||||
contains_eager(Product.product_group),
|
contains_eager(ProductVersion.product_group),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.unique()
|
.unique()
|
||||||
@@ -226,30 +407,64 @@ async def show_term_sku(
|
|||||||
a: bool | None = None, # Active
|
a: bool | None = None, # Active
|
||||||
p: bool | None = None, # Is Purchased?
|
p: bool | None = None, # Is Purchased?
|
||||||
v: uuid.UUID | None = None, # Vendor
|
v: uuid.UUID | None = None, # Vendor
|
||||||
d: str | None = None, # Date
|
date_: date = Depends(effective_date), # Date
|
||||||
current_user: UserToken = Depends(get_user),
|
current_user: UserToken = Depends(get_user),
|
||||||
) -> list[ProductSku]:
|
) -> list[ProductSku]:
|
||||||
list_ = []
|
list_ = []
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
query_ = select(Product).join(Product.skus).options(contains_eager(Product.skus))
|
sku_version_onclause = and_(
|
||||||
if a is not None:
|
SkuVersion.sku_id == StockKeepingUnit.id,
|
||||||
query_ = query_.filter(Product.is_active == a)
|
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:
|
if p is not None:
|
||||||
query_ = query_.filter(Product.is_purchased == p)
|
query_ = query_.filter(ProductVersion.is_purchased == p)
|
||||||
if q is not None:
|
if q is not None:
|
||||||
for sub in q.split():
|
for sub in q.split():
|
||||||
if sub.strip() != "":
|
if sub.strip() != "":
|
||||||
query_ = query_.filter(
|
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 item in db.execute(query_).unique().scalars().all():
|
||||||
for sku in item.skus:
|
for sku in [sku.versions[0] for sku in item.product.skus]:
|
||||||
rc_price = get_rc_price(item.id, d, v, db)
|
rc_price = get_rc_price(item.id, date_, v, db)
|
||||||
list_.append(
|
list_.append(
|
||||||
ProductSku(
|
ProductSku(
|
||||||
id_=sku.id,
|
id_=sku.sku_id,
|
||||||
name=f"{item.name} ({sku.units})",
|
name=f"{item.name} ({sku.units})",
|
||||||
fraction_units=item.fraction_units,
|
fraction_units=item.fraction_units,
|
||||||
cost_price=sku.cost_price if rc_price is None else rc_price,
|
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
|
q: str | None = None, # Query
|
||||||
a: bool | None = None, # Active
|
a: bool | None = None, # Active
|
||||||
p: bool | None = None, # Is Purchased?
|
p: bool | None = None, # Is Purchased?
|
||||||
|
date_: date = Depends(effective_date), # Date
|
||||||
current_user: UserToken = Depends(get_user),
|
current_user: UserToken = Depends(get_user),
|
||||||
) -> list[ProductSku]:
|
) -> list[ProductSku]:
|
||||||
list_ = []
|
list_ = []
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
query_ = select(Product)
|
query_ = select(ProductVersion).where(
|
||||||
if a is not None:
|
and_(
|
||||||
query_ = query_.filter(Product.is_active == a)
|
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:
|
if p is not None:
|
||||||
query_ = query_.filter(Product.is_purchased == p)
|
query_ = query_.filter(ProductVersion.is_purchased == p)
|
||||||
if q is not None:
|
if q is not None:
|
||||||
for sub in q.split():
|
for sub in q.split():
|
||||||
if sub.strip() != "":
|
if sub.strip() != "":
|
||||||
query_ = query_.filter(Product.name.ilike(f"%{sub}%"))
|
query_ = query_.filter(ProductVersion.name.ilike(f"%{sub}%"))
|
||||||
query_ = query_.order_by(Product.name)
|
query_ = query_.order_by(ProductVersion.name)
|
||||||
|
|
||||||
for item in db.execute(query_).unique().scalars().all():
|
for item in db.execute(query_).unique().scalars().all():
|
||||||
list_.append(
|
list_.append(
|
||||||
@@ -297,20 +525,57 @@ async def show_term_product(
|
|||||||
@router.get("/{id_}", response_model=schemas.Product)
|
@router.get("/{id_}", response_model=schemas.Product)
|
||||||
def show_id(
|
def show_id(
|
||||||
id_: uuid.UUID,
|
id_: uuid.UUID,
|
||||||
|
date_: date = Depends(effective_date),
|
||||||
user: UserToken = Security(get_user, scopes=["products"]),
|
user: UserToken = Security(get_user, scopes=["products"]),
|
||||||
) -> schemas.Product:
|
) -> schemas.Product:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
|
version: ProductVersion = db.execute(
|
||||||
return product_info(item)
|
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(
|
return schemas.Product(
|
||||||
id_=product.id,
|
id_=version.product_id,
|
||||||
code=product.code,
|
handle=version.handle,
|
||||||
name=product.name,
|
name=version.name,
|
||||||
description=product.description,
|
description=version.description,
|
||||||
fraction_units=product.fraction_units,
|
fraction_units=version.fraction_units,
|
||||||
skus=[
|
skus=[
|
||||||
schemas.StockKeepingUnit(
|
schemas.StockKeepingUnit(
|
||||||
id_=sku.id,
|
id_=sku.id,
|
||||||
@@ -320,27 +585,27 @@ def product_info(product: Product) -> schemas.Product:
|
|||||||
cost_price=sku.cost_price,
|
cost_price=sku.cost_price,
|
||||||
sale_price=sku.sale_price,
|
sale_price=sku.sale_price,
|
||||||
)
|
)
|
||||||
for sku in product.skus
|
for sku in skus
|
||||||
],
|
],
|
||||||
is_active=product.is_active,
|
is_fixture=version.product.is_fixture,
|
||||||
is_fixture=product.is_fixture,
|
is_purchased=version.is_purchased,
|
||||||
is_purchased=product.is_purchased,
|
is_sold=version.is_sold,
|
||||||
is_sold=product.is_sold,
|
is_active=True,
|
||||||
product_group=schemas.ProductGroupLink(id_=product.product_group.id, name=product.product_group.name),
|
product_group=schemas.ProductGroupLink(id_=version.product_group.id, name=version.product_group.name),
|
||||||
allergen=product.allergen,
|
allergen=version.allergen,
|
||||||
protein=product.protein,
|
protein=version.protein,
|
||||||
carbohydrate=product.carbohydrate,
|
carbohydrate=version.carbohydrate,
|
||||||
total_sugar=product.total_sugar,
|
total_sugar=version.total_sugar,
|
||||||
added_sugar=product.added_sugar,
|
added_sugar=version.added_sugar,
|
||||||
total_fat=product.total_fat,
|
total_fat=version.total_fat,
|
||||||
saturated_fat=product.saturated_fat,
|
saturated_fat=version.saturated_fat,
|
||||||
trans_fat=product.trans_fat,
|
trans_fat=version.trans_fat,
|
||||||
cholestrol=product.cholestrol,
|
cholestrol=version.cholestrol,
|
||||||
sodium=product.sodium,
|
sodium=version.sodium,
|
||||||
msnf=product.msnf,
|
msnf=version.msnf,
|
||||||
other_solids=product.other_solids,
|
other_solids=version.other_solids,
|
||||||
total_solids=product.total_solids,
|
total_solids=version.total_solids,
|
||||||
water=product.water,
|
water=version.water,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -350,10 +615,10 @@ def product_blank() -> schemas.ProductBlank:
|
|||||||
description="",
|
description="",
|
||||||
fraction_units="",
|
fraction_units="",
|
||||||
skus=[],
|
skus=[],
|
||||||
is_active=True,
|
|
||||||
is_purchased=True,
|
is_purchased=True,
|
||||||
is_sold=False,
|
is_sold=False,
|
||||||
is_fixture=False,
|
is_fixture=False,
|
||||||
|
is_active=True,
|
||||||
allergen="",
|
allergen="",
|
||||||
protein=Decimal(0),
|
protein=Decimal(0),
|
||||||
carbohydrate=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:
|
def get_rc_price(id_: uuid.UUID, date_: date, v: uuid.UUID | None, db: Session) -> Decimal | None:
|
||||||
if d is None or v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
date_ = datetime.strptime(d, "%d-%b-%Y")
|
|
||||||
contracts = select(RateContract.id).where(
|
contracts = select(RateContract.id).where(
|
||||||
RateContract.vendor_id == v, RateContract.valid_from <= date_, RateContract.valid_till >= date_
|
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 decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
|
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.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
import brewman.schemas.input as schema_in
|
import brewman.schemas.input as schema_in
|
||||||
import brewman.schemas.voucher as output
|
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 brewman.routers.tag import save_tags, update_tags
|
||||||
|
|
||||||
from ..core.security import get_current_active_user as get_user
|
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:
|
def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
|
||||||
product_accounts = (
|
product_ids = select(func.distinct(StockKeepingUnit.product_id)).where(
|
||||||
select(Product.account_id)
|
StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories])
|
||||||
.join(Product.skus)
|
)
|
||||||
.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(
|
account_types: list[int] = list(
|
||||||
db.execute(
|
db.execute(
|
||||||
@@ -138,7 +151,23 @@ def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
|
|||||||
tax=item.tax,
|
tax=item.tax,
|
||||||
discount=item.discount,
|
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)
|
voucher.inventories.append(inventory)
|
||||||
db.add(inventory)
|
db.add(inventory)
|
||||||
|
|
||||||
@@ -148,11 +177,22 @@ def save_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) ->
|
|||||||
journals: dict[uuid.UUID, Journal] = {}
|
journals: dict[uuid.UUID, Journal] = {}
|
||||||
amount = Decimal(0)
|
amount = Decimal(0)
|
||||||
for i_item in voucher.inventories:
|
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(
|
account_id, cc_id = db.execute(
|
||||||
select(AccountBase.id, AccountBase.cost_centre_id)
|
select(AccountBase.id, AccountBase.cost_centre_id).where(AccountBase.id == product_account)
|
||||||
.join(AccountBase.products)
|
|
||||||
.join(Product.skus)
|
|
||||||
.where(StockKeepingUnit.id == i_item.batch.sku.id)
|
|
||||||
).one()
|
).one()
|
||||||
amount += round(i_item.amount, 2)
|
amount += round(i_item.amount, 2)
|
||||||
if account_id in journals:
|
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:
|
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()
|
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
|
||||||
product_accounts = (
|
product_ids = select(func.distinct(StockKeepingUnit.product_id)).where(
|
||||||
select(Product.account_id)
|
StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories])
|
||||||
.join(Product.skus)
|
|
||||||
.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(
|
account_types: list[int] = list(
|
||||||
db.execute(
|
db.execute(
|
||||||
select(distinct(AccountBase.type_id)).where(
|
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),
|
(idx for (idx, d) in enumerate(inventories) if d.id_ == item.id and d.batch.sku.id_ == item.batch.sku_id),
|
||||||
None,
|
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:
|
if index is not None:
|
||||||
new_inventory = inventories.pop(index)
|
new_inventory = inventories.pop(index)
|
||||||
sku: StockKeepingUnit = db.execute(
|
sku: StockKeepingUnit = db.execute(
|
||||||
select(StockKeepingUnit).where(StockKeepingUnit.id == item.batch.sku.id)
|
select(StockKeepingUnit).where(StockKeepingUnit.id == item.batch.sku.id)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
|
|
||||||
rc_price = rate_contract_price(sku.id, vendor_id, voucher.date_, db)
|
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):
|
if batch_has_older_vouchers(item.batch_id, voucher.date_, voucher.id, db):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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:
|
if rc_price is not None and rc_price != new_inventory.rate:
|
||||||
raise HTTPException(
|
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.batch.discount = new_inventory.discount
|
||||||
item.tax = new_inventory.tax
|
item.tax = new_inventory.tax
|
||||||
item.batch.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()
|
db.flush()
|
||||||
fix_single_batch_prices(item.batch_id, db)
|
fix_single_batch_prices(item.batch_id, db)
|
||||||
else:
|
else:
|
||||||
@@ -290,7 +374,7 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
|
|||||||
if has_been_issued > 0:
|
if has_been_issued > 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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:
|
else:
|
||||||
db.delete(item.batch)
|
db.delete(item.batch)
|
||||||
@@ -304,9 +388,21 @@ def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) -
|
|||||||
journals: dict[uuid.UUID, Journal] = {}
|
journals: dict[uuid.UUID, Journal] = {}
|
||||||
amount = Decimal(0)
|
amount = Decimal(0)
|
||||||
for i_item in voucher.inventories:
|
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(
|
account_id, cc_id = db.execute(
|
||||||
select(AccountBase.id, AccountBase.cost_centre_id)
|
select(AccountBase.id, AccountBase.cost_centre_id)
|
||||||
.join(AccountBase.products)
|
.join(AccountBase.products)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.join(Product.skus)
|
||||||
.where(StockKeepingUnit.id == i_item.batch.sku.id)
|
.where(StockKeepingUnit.id == i_item.batch.sku.id)
|
||||||
).one()
|
).one()
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ from datetime import UTC, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
|
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.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.input as schema_in
|
||||||
import brewman.schemas.voucher as output
|
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 brewman.routers.tag import save_tags, update_tags
|
||||||
|
|
||||||
from ..core.security import get_current_active_user as get_user
|
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:
|
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 = (
|
product_accounts = (
|
||||||
select(Product.account_id)
|
select(ProductVersion.account_id)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.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]))
|
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
|
||||||
)
|
)
|
||||||
account_types: list[int] = list(
|
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:
|
def save_inventories(voucher: Voucher, inventories: list[InventorySchema], db: Session) -> None:
|
||||||
for d_item in inventories:
|
for d_item in inventories:
|
||||||
batch = db.execute(select(Batch).where(Batch.id == d_item.batch.id_)).scalar_one()
|
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:
|
if d_item.quantity > batch.quantity_remaining:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
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_:
|
if batch.name > voucher.date_:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail=f"{batch.sku.product.name} ({batch.sku.units}) "
|
detail=f"{product_name} ({product_unit}) was purchased on {batch.name.strftime('%d-%b-%Y')}",
|
||||||
f"was purchased on {batch.name.strftime('%d-%b-%Y')}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
batch.quantity_remaining -= d_item.quantity
|
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] = {}
|
journals: dict[uuid.UUID, Journal] = {}
|
||||||
amount = Decimal(0)
|
amount = Decimal(0)
|
||||||
for v_item in voucher.inventories:
|
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(
|
account_id, cc_id = db.execute(
|
||||||
select(AccountBase.id, AccountBase.cost_centre_id)
|
select(AccountBase.id, AccountBase.cost_centre_id)
|
||||||
.join(AccountBase.products)
|
.join(AccountBase.products)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.join(Product.skus)
|
||||||
.where(StockKeepingUnit.id == v_item.batch.sku.id)
|
.where(StockKeepingUnit.id == v_item.batch.sku.id)
|
||||||
).one()
|
).one()
|
||||||
@@ -199,11 +258,28 @@ def update_route(
|
|||||||
|
|
||||||
def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
|
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()
|
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 = (
|
product_accounts = (
|
||||||
select(Product.account_id)
|
select(ProductVersion.account_id)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.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]))
|
.where(StockKeepingUnit.id.in_([i.batch.sku.id_ for i in data.inventories]))
|
||||||
)
|
)
|
||||||
|
|
||||||
account_types: list[int] = list(
|
account_types: list[int] = list(
|
||||||
db.execute(
|
db.execute(
|
||||||
select(distinct(AccountBase.type_id)).where(
|
select(distinct(AccountBase.type_id)).where(
|
||||||
@@ -240,13 +316,42 @@ def update_inventory(voucher: Voucher, new_inventories: list[InventorySchema], d
|
|||||||
index = next(
|
index = next(
|
||||||
(idx for (idx, d) in enumerate(new_inventories) if d.id_ == item.id and d.batch.id_ == item.batch.id), None
|
(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:
|
if index is not None:
|
||||||
new_inventory = new_inventories.pop(index)
|
new_inventory = new_inventories.pop(index)
|
||||||
if new_inventory.quantity > batch_quantity:
|
if new_inventory.quantity > batch_quantity:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail=f"Maximum quantity available for "
|
detail=f"Maximum quantity available for {product_name} ({product_unit}) is {batch_quantity}",
|
||||||
f"{batch.sku.product.name} ({batch.sku.units}) is {batch_quantity}",
|
|
||||||
)
|
)
|
||||||
if batch.name > voucher.date_:
|
if batch.name > voucher.date_:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -270,9 +375,21 @@ def update_journals(voucher: Voucher, ven: schema_in.AccountLink, db: Session) -
|
|||||||
journals: dict[uuid.UUID, Journal] = {}
|
journals: dict[uuid.UUID, Journal] = {}
|
||||||
amount = Decimal(0)
|
amount = Decimal(0)
|
||||||
for v_item in voucher.inventories:
|
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(
|
account_id, cc_id = db.execute(
|
||||||
select(AccountBase.id, AccountBase.cost_centre_id)
|
select(AccountBase.id, AccountBase.cost_centre_id)
|
||||||
.join(AccountBase.products)
|
.join(AccountBase.products)
|
||||||
|
.join(Product, onclause=product_version_onclause)
|
||||||
.join(Product.skus)
|
.join(Product.skus)
|
||||||
.where(StockKeepingUnit.id == v_item.batch.sku.id)
|
.where(StockKeepingUnit.id == v_item.batch.sku.id)
|
||||||
).one()
|
).one()
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Security, status
|
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.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
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.security import get_current_active_user as get_user
|
||||||
from ..core.session import (
|
from ..core.session import (
|
||||||
get_date,
|
get_date,
|
||||||
@@ -50,7 +55,7 @@ async def save(
|
|||||||
set_date(data.date_, request.session)
|
set_date(data.date_, request.session)
|
||||||
set_period(data.valid_from, data.valid_till, request.session)
|
set_period(data.valid_from, data.valid_till, request.session)
|
||||||
db.commit()
|
db.commit()
|
||||||
return rate_contract_info(item)
|
return rate_contract_info(item, db)
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -85,7 +90,7 @@ async def update_route(
|
|||||||
set_date(data.date_, request.session)
|
set_date(data.date_, request.session)
|
||||||
set_period(data.valid_from, data.valid_till, request.session)
|
set_period(data.valid_from, data.valid_till, request.session)
|
||||||
db.commit()
|
db.commit()
|
||||||
return rate_contract_info(item)
|
return rate_contract_info(item, db)
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -134,7 +139,7 @@ async def show_list(
|
|||||||
) -> list[RateContractSchema]:
|
) -> list[RateContractSchema]:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
return [
|
return [
|
||||||
rate_contract_info(item)
|
rate_contract_info(item, db)
|
||||||
for item in db.execute(select(RateContract).order_by(RateContract.date_)).scalars().all()
|
for item in db.execute(select(RateContract).order_by(RateContract.date_)).scalars().all()
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -146,10 +151,10 @@ async def show_id(
|
|||||||
) -> RateContractSchema:
|
) -> RateContractSchema:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
|
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(
|
return RateContractSchema(
|
||||||
id_=item.id,
|
id_=item.id,
|
||||||
date_=item.date_,
|
date_=item.date_,
|
||||||
@@ -163,9 +168,7 @@ def rate_contract_info(item: RateContract) -> RateContractSchema:
|
|||||||
items=[
|
items=[
|
||||||
RateContractItemSchema(
|
RateContractItemSchema(
|
||||||
id_=i.id,
|
id_=i.id,
|
||||||
sku=ProductLink(
|
sku=ProductLink(id_=i.sku_id, name=get_product_name(i.sku, item.date_, db)),
|
||||||
id_=i.sku_id, name=f"{i.sku.product.name} ({i.sku.units})" if i.sku.units else i.sku.product.name
|
|
||||||
),
|
|
||||||
price=i.price,
|
price=i.price,
|
||||||
)
|
)
|
||||||
for i in item.items
|
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:
|
def rate_contract_blank(session: dict[str, Any]) -> RateContractBlank:
|
||||||
return RateContractBlank(
|
return RateContractBlank(
|
||||||
date_=get_date(session), # type: ignore[arg-type]
|
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:
|
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)
|
sum_func = func.sum(Journal.signed_amount)
|
||||||
query = db.execute(
|
query = db.execute(
|
||||||
select(AccountBase, sum_func)
|
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:
|
for account, amount in query:
|
||||||
amount = round(Decimal(amount), 2)
|
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
|
running_total += amount
|
||||||
journal = Journal(
|
journal = Journal(
|
||||||
amount=abs(amount),
|
amount=abs(amount),
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
|
|||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.styles import Alignment, Border, Font, NamedStyle, PatternFill, Side
|
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.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 as schemas
|
||||||
import brewman.schemas.recipe_item as rischemas
|
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 ..core.security import get_current_active_user as get_user
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
from ..models.price import Price
|
from ..models.price import Price
|
||||||
@@ -207,11 +210,44 @@ async def show_list(
|
|||||||
user: UserToken = Depends(get_user),
|
user: UserToken = Depends(get_user),
|
||||||
) -> list[schemas.Recipe]:
|
) -> list[schemas.Recipe]:
|
||||||
with SessionFuture() as db:
|
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] = (
|
list_: Sequence[Recipe] = (
|
||||||
db.execute(
|
db.execute(
|
||||||
select(Recipe)
|
select(Recipe)
|
||||||
.options(joinedload(Recipe.sku, innerjoin=True).joinedload(StockKeepingUnit.product, innerjoin=True))
|
.join(Recipe.sku)
|
||||||
.order_by(Recipe.sku_id)
|
.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()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
@@ -219,7 +255,7 @@ async def show_list(
|
|||||||
return [
|
return [
|
||||||
schemas.Recipe(
|
schemas.Recipe(
|
||||||
id_=item.id,
|
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_,
|
date_=item.date_,
|
||||||
source=item.source,
|
source=item.source,
|
||||||
instructions=item.instructions,
|
instructions=item.instructions,
|
||||||
@@ -227,8 +263,8 @@ async def show_list(
|
|||||||
plating=item.plating,
|
plating=item.plating,
|
||||||
recipe_yield=item.recipe_yield,
|
recipe_yield=item.recipe_yield,
|
||||||
notes=item.notes,
|
notes=item.notes,
|
||||||
product_group_id=item.sku.product.product_group_id,
|
product_group_id=item.sku.product.versions[0].product_group_id,
|
||||||
units=item.sku.units,
|
units=item.sku.versions[0].units,
|
||||||
items=[],
|
items=[],
|
||||||
)
|
)
|
||||||
for item in list_
|
for item in list_
|
||||||
@@ -267,7 +303,7 @@ def show_pdf(
|
|||||||
|
|
||||||
@router.get("/xlsx", response_class=StreamingResponse)
|
@router.get("/xlsx", response_class=StreamingResponse)
|
||||||
def get_report(
|
def get_report(
|
||||||
p: uuid.UUID | None = None,
|
p: uuid.UUID,
|
||||||
t: uuid.UUID | None = None,
|
t: uuid.UUID | None = None,
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
with SessionFuture() as db:
|
with SessionFuture() as db:
|
||||||
@@ -281,24 +317,79 @@ def get_report(
|
|||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.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] = []
|
list_: Sequence[Recipe] = []
|
||||||
with SessionFuture() as db:
|
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 = (
|
q = (
|
||||||
select(Recipe)
|
select(Recipe)
|
||||||
|
.join(Recipe.items)
|
||||||
|
.join(RecipeItem.product)
|
||||||
|
.join(ItemProductVersion, item_product_version_onclause)
|
||||||
.join(Recipe.sku)
|
.join(Recipe.sku)
|
||||||
|
.join(CurrentSkuVersion, sku_version_onclause)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
.join(Product.product_group)
|
.join(RecipeProductVersion, recipe_product_version_onclause)
|
||||||
|
.join(RecipeProductVersion.product_group)
|
||||||
.options(
|
.options(
|
||||||
joinedload(Recipe.items, innerjoin=True).joinedload(RecipeItem.product, innerjoin=True),
|
# Eagerly load ingredients and their product & product_version
|
||||||
contains_eager(Recipe.sku, StockKeepingUnit.product, Product.product_group),
|
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:
|
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()
|
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)
|
e.seek(0)
|
||||||
|
|
||||||
headers = {"Content-Disposition": "attachment; filename = recipe.xlsx"}
|
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=2, value=p[1])
|
||||||
wb.active.cell(row=i, column=3, value=p[2])
|
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:
|
for pg in pgs:
|
||||||
wb.create_sheet(pg)
|
wb.create_sheet(pg)
|
||||||
rows = defaultdict(lambda: 1)
|
rows = defaultdict(lambda: 1)
|
||||||
register_styles(wb)
|
register_styles(wb)
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
ws = wb[recipe.sku.product.product_group.name]
|
ws = wb[recipe.sku.product.versions[0].product_group.name]
|
||||||
row = rows[recipe.sku.product.product_group.name]
|
row = rows[recipe.sku.product.versions[0].product_group.name]
|
||||||
print(row)
|
print(row)
|
||||||
ings = len(recipe.items)
|
ings = len(recipe.items)
|
||||||
ing_from = row + 2
|
ing_from = row + 2
|
||||||
ing_till = ing_from + ings - 1
|
ing_till = ing_from + ings - 1
|
||||||
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=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=3, value=recipe.recipe_yield).style = "recipe_name"
|
||||||
ws.cell(row=row, column=4).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"
|
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"
|
ws.cell(row=row, column=5, value="Amount").style = "header"
|
||||||
for item in recipe.items:
|
for item in recipe.items:
|
||||||
row += 1
|
row += 1
|
||||||
ws.cell(row=row, column=1, value=item.product.name).style = "ing"
|
ws.cell(row=row, column=1, value=item.product.versions[0].name).style = "ing"
|
||||||
ws.cell(row=row, column=2, value=item.product.fraction_units).style = "unit"
|
ws.cell(row=row, column=2, value=item.product.versions[0].fraction_units).style = "unit"
|
||||||
ws.cell(row=row, column=3, value=item.quantity).style = "ing"
|
ws.cell(row=row, column=3, value=item.quantity).style = "ing"
|
||||||
ws.cell(row=row, column=4, value="=VLOOKUP(A:A,'Rate List'!A:C,3,0)").style = "ing"
|
ws.cell(row=row, column=4, value="=VLOOKUP(A:A,'Rate List'!A:C,3,0)").style = "ing"
|
||||||
ws.cell(row=row, column=5, value=f"=C{row}*D{row}").style = "ing"
|
ws.cell(row=row, column=5, value=f"=C{row}*D{row}").style = "ing"
|
||||||
rows[recipe.sku.product.product_group.name] = row + 1
|
rows[recipe.sku.product.versions[0].product_group.name] = row + 1
|
||||||
virtual_workbook = BytesIO()
|
virtual_workbook = BytesIO()
|
||||||
wb.save(virtual_workbook)
|
wb.save(virtual_workbook)
|
||||||
return virtual_workbook
|
return virtual_workbook
|
||||||
@@ -466,7 +557,7 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe:
|
|||||||
id_=recipe.id,
|
id_=recipe.id,
|
||||||
sku=schemas.ProductLink(
|
sku=schemas.ProductLink(
|
||||||
id_=recipe.sku_id,
|
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_,
|
date_=recipe.date_,
|
||||||
source=recipe.source,
|
source=recipe.source,
|
||||||
@@ -480,7 +571,7 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe:
|
|||||||
id_=item.id,
|
id_=item.id,
|
||||||
product=schemas.ProductLink(
|
product=schemas.ProductLink(
|
||||||
id_=item.product.id,
|
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),
|
quantity=round(item.quantity, 2),
|
||||||
description=item.description,
|
description=item.description,
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ from datetime import date, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Security, status
|
from fastapi import APIRouter, HTTPException, Request, Security, status
|
||||||
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session, contains_eager
|
from sqlalchemy.orm import Session, contains_eager
|
||||||
from sqlalchemy.sql.expression import delete, distinct, func, or_, select, update
|
from sqlalchemy.sql.expression import delete, distinct, func, or_, select, update
|
||||||
|
|
||||||
import brewman.schemas.closing_stock as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_date, set_period
|
||||||
from ...db.session import SessionFuture
|
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]:
|
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")
|
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")
|
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
|
||||||
query = db.execute(
|
product_version_onclause = and_(
|
||||||
select(StockKeepingUnit, quantity_sum, amount_sum)
|
ProductVersion.product_id == Product.id,
|
||||||
.join(StockKeepingUnit.product)
|
or_(
|
||||||
.join(Product.product_group)
|
ProductVersion.valid_from == None, # noqa: E711
|
||||||
.join(StockKeepingUnit.batches)
|
ProductVersion.valid_from <= Voucher.date_,
|
||||||
.join(Batch.inventories)
|
),
|
||||||
.join(Inventory.voucher)
|
or_(
|
||||||
.join(Voucher.journals)
|
ProductVersion.valid_till == None, # noqa: E711
|
||||||
.options(contains_eager(StockKeepingUnit.product).contains_eager(Product.product_group))
|
ProductVersion.valid_till >= Voucher.date_,
|
||||||
.where(
|
),
|
||||||
Voucher.date_ <= date_,
|
)
|
||||||
Journal.cost_centre_id == cost_centre_id,
|
|
||||||
or_(Voucher.voucher_type != VoucherType.CLOSING_STOCK, Voucher.date_ != 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
|
.unique()
|
||||||
.order_by(ProductGroup.name, Product.name, StockKeepingUnit.units)
|
.all()
|
||||||
).all()
|
)
|
||||||
|
|
||||||
physical_list = (
|
physical_list = (
|
||||||
db.execute(
|
db.execute(
|
||||||
@@ -179,16 +218,18 @@ def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> list[sc
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
body = []
|
body = []
|
||||||
for sku, quantity, amount in query:
|
for sku_version, quantity, amount in query:
|
||||||
if quantity != 0 and amount != 0:
|
if quantity != 0 and amount != 0:
|
||||||
id_ = next((p.id for p in physical_list if p.sku_id == 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.id), quantity)
|
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.id), None)
|
cc = next((schemas.CostCentreLink(id_=c) for (c, s) in ccs if s == sku_version.sku_id), None)
|
||||||
body.append(
|
body.append(
|
||||||
schemas.ClosingStockItem(
|
schemas.ClosingStockItem(
|
||||||
id_=id_,
|
id_=id_,
|
||||||
product=schemas.ProductLink(id_=sku.id, name=f"{sku.product.name} ({sku.units})"),
|
product=schemas.ProductLink(
|
||||||
group=sku.product.product_group.name,
|
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,
|
quantity=quantity,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
physical=physical,
|
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:
|
def save(date_: date, items: list[schemas.ClosingStockItem], user: UserToken, db: Session) -> Voucher:
|
||||||
product_accounts: list[uuid.UUID] = list( # type: ignore
|
product_version_onclause = and_(
|
||||||
select(Product.account_id).join(Product.skus).where(StockKeepingUnit.id.in_([i.product.id_ for i in items]))
|
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(
|
account_types: list[int] = list(
|
||||||
db.execute(select(distinct(AccountBase.type_id)).where(AccountBase.id.in_(product_accounts))).scalars().all()
|
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 decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Security, status
|
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.exc import SQLAlchemyError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.sql.expression import select
|
from sqlalchemy.sql.expression import select
|
||||||
|
|
||||||
import brewman.schemas.mozimo_daily_register as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_date, set_date
|
from ...core.session import get_date, set_date
|
||||||
from ...db.session import SessionFuture
|
from ...db.session import SessionFuture
|
||||||
@@ -42,30 +45,55 @@ def show_data(
|
|||||||
body = build_report(d, db)
|
body = build_report(d, db)
|
||||||
set_date(date_, request.session)
|
set_date(date_, request.session)
|
||||||
return schemas.MozimoDailyRegister(
|
return schemas.MozimoDailyRegister(
|
||||||
date=d,
|
date_=d,
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterItem]:
|
def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterItem]:
|
||||||
body = []
|
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 = (
|
products = (
|
||||||
db.execute(
|
db.execute(
|
||||||
select(StockKeepingUnit)
|
select(ProductVersion.name, SkuVersion.units, SkuVersion.sku_id)
|
||||||
.join(StockKeepingUnit.product)
|
.select_from(Product)
|
||||||
.where(Product.product_group_id == uuid.UUID("dad46805-f577-4e5b-8073-9b788e0173fc")) # Menu items
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
.order_by(Product.name, StockKeepingUnit.units)
|
.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()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
for sku in products:
|
for name, units, sku_id in products:
|
||||||
ob = opening_balance(sku.id, date_, db)
|
ob = opening_balance(sku_id, date_, db)
|
||||||
item = (
|
item = (
|
||||||
db.execute(
|
db.execute(
|
||||||
select(MozimoStockRegister).where(
|
select(MozimoStockRegister).where(
|
||||||
MozimoStockRegister.sku_id == sku.id,
|
MozimoStockRegister.sku_id == sku_id,
|
||||||
MozimoStockRegister.date_ == date_,
|
MozimoStockRegister.date_ == date_,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -75,7 +103,7 @@ def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterIt
|
|||||||
body.append(
|
body.append(
|
||||||
schemas.MozimoDailyRegisterItem(
|
schemas.MozimoDailyRegisterItem(
|
||||||
id_=None if item is None else item.id_,
|
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,
|
opening=ob,
|
||||||
received=Decimal(0) if item is None else item.received,
|
received=Decimal(0) if item is None else item.received,
|
||||||
sale=Decimal(0) if item is None else item.sale,
|
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()
|
.scalars()
|
||||||
.one_or_none()
|
.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(
|
query_ = select(func.sum(MozimoStockRegister.received - MozimoStockRegister.sale - MozimoStockRegister.nc)).where(
|
||||||
MozimoStockRegister.sku_id == product_id,
|
MozimoStockRegister.sku_id == product_id,
|
||||||
MozimoStockRegister.date_ < start_date,
|
MozimoStockRegister.date_ < start_date,
|
||||||
)
|
)
|
||||||
if opening_physical is not None:
|
if opening_physical is not None:
|
||||||
query_ = query_.where(MozimoStockRegister.date_ > opening_physical.date_)
|
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
|
return physical + calculated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def show_data(
|
|||||||
return schemas.MozimoProductRegister(
|
return schemas.MozimoProductRegister(
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
finish_date=finish_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,
|
body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ def build_report(
|
|||||||
)
|
)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
if item.ageing is not None or item.display 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:
|
else:
|
||||||
closing = ob + item.received - item.sale - item.nc
|
closing = ob + item.received - item.sale - item.nc
|
||||||
ob = closing # Setting the cb as ob for next iteration
|
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)
|
ob = opening_balance(data.product.id_, data.start_date, db)
|
||||||
for item in data.body:
|
for item in data.body:
|
||||||
if item.ageing is not None or item.display 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:
|
else:
|
||||||
closing = ob + item.received - item.sale - item.nc
|
closing = ob + item.received - item.sale - item.nc
|
||||||
ob = closing # Setting the cb as ob for next iteration
|
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 fastapi import APIRouter, Security
|
||||||
from sqlalchemy import select
|
from sqlalchemy import and_, or_, select
|
||||||
from sqlalchemy.orm import Session, contains_eager, joinedload
|
from sqlalchemy.orm import Session, contains_eager, joinedload
|
||||||
|
|
||||||
import brewman.schemas.non_contract_purchase as schemas
|
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.inventory import Inventory
|
||||||
from brewman.models.journal import Journal
|
from brewman.models.journal import Journal
|
||||||
from brewman.models.product import Product
|
from brewman.models.product import Product
|
||||||
|
from brewman.models.product_version import ProductVersion
|
||||||
from brewman.models.rate_contract import RateContract
|
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.stock_keeping_unit import StockKeepingUnit
|
||||||
from brewman.models.voucher import Voucher
|
from brewman.models.voucher import Voucher
|
||||||
from brewman.models.voucher_type import VoucherType
|
from brewman.models.voucher_type import VoucherType
|
||||||
@@ -46,13 +48,41 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
list_: list[schemas.NonContractPurchase] = []
|
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 rc in rcs:
|
||||||
for item in rc.items:
|
for item in rc.items:
|
||||||
invs = db.execute(
|
invs = db.execute(
|
||||||
select(Voucher.id, Voucher.date_, Inventory.id, Inventory.rate)
|
select(Voucher.id, Voucher.date_, Inventory.rate, ProductVersion.name, SkuVersion.units)
|
||||||
.join(Inventory.batch)
|
.select_from(Voucher)
|
||||||
.join(Inventory.voucher)
|
|
||||||
.join(Voucher.journals)
|
.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(
|
.where(
|
||||||
Batch.sku_id == item.sku_id,
|
Batch.sku_id == item.sku_id,
|
||||||
Voucher.date_ >= rc.valid_from,
|
Voucher.date_ >= rc.valid_from,
|
||||||
@@ -62,24 +92,19 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
|
|||||||
Inventory.rate != item.price,
|
Inventory.rate != item.price,
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
for inv in invs:
|
for voucher_id, date_, rate, name, units in invs:
|
||||||
p, u = db.execute(
|
|
||||||
select(Product.name, StockKeepingUnit.units)
|
|
||||||
.join(Product.skus)
|
|
||||||
.where(StockKeepingUnit.id == item.sku_id)
|
|
||||||
).one()
|
|
||||||
list_.append(
|
list_.append(
|
||||||
schemas.NonContractPurchase(
|
schemas.NonContractPurchase(
|
||||||
date_=inv.date_,
|
date_=date_,
|
||||||
vendor=rc.vendor.name,
|
vendor=rc.vendor.name,
|
||||||
product=f"{p} ({u})",
|
product=f"{name} ({units})",
|
||||||
url=[
|
url=[
|
||||||
"/",
|
"/",
|
||||||
"purchase",
|
"purchase",
|
||||||
str(inv.id),
|
str(voucher_id),
|
||||||
],
|
],
|
||||||
contract_price=item.price,
|
contract_price=item.price,
|
||||||
purchase_price=inv.rate,
|
purchase_price=rate,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return list_
|
return list_
|
||||||
@@ -4,11 +4,14 @@ from datetime import date, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Security
|
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
|
from sqlalchemy.sql.expression import func, select
|
||||||
|
|
||||||
import brewman.schemas.product_ledger as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_period
|
||||||
from ...db.session import SessionFuture
|
from ...db.session import SessionFuture
|
||||||
@@ -61,7 +64,7 @@ def show_data(
|
|||||||
return schemas.ProductLedger(
|
return schemas.ProductLedger(
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
finish_date=finish_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,
|
body=body,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,16 +75,31 @@ def build_report(
|
|||||||
running_total_q, running_total_a, opening = opening_balance(product_id, start_date, db)
|
running_total_q, running_total_a, opening = opening_balance(product_id, start_date, db)
|
||||||
body = opening
|
body = opening
|
||||||
|
|
||||||
query = db.execute(
|
sku_version_onclause = and_(
|
||||||
select(Voucher, Inventory, Journal, StockKeepingUnit)
|
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(Voucher.journals)
|
||||||
|
.join(Journal.account)
|
||||||
|
.join(Journal.cost_centre)
|
||||||
.join(Voucher.inventories)
|
.join(Voucher.inventories)
|
||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
.options(
|
.options(
|
||||||
joinedload(Journal.account, innerjoin=True),
|
contains_eager(Voucher.journals).contains_eager(Journal.account),
|
||||||
joinedload(Journal.cost_centre, innerjoin=True),
|
contains_eager(Voucher.journals).contains_eager(Journal.cost_centre),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
StockKeepingUnit.product_id == product_id,
|
StockKeepingUnit.product_id == product_id,
|
||||||
@@ -141,16 +159,30 @@ def build_report(
|
|||||||
def opening_balance(
|
def opening_balance(
|
||||||
product_id: uuid.UUID, start_date: date, db: Session
|
product_id: uuid.UUID, start_date: date, db: Session
|
||||||
) -> tuple[Decimal, Decimal, list[schemas.ProductLedgerItem]]:
|
) -> 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(
|
row = db.execute(
|
||||||
select(
|
select(
|
||||||
StockKeepingUnit.units,
|
SkuVersion.units,
|
||||||
func.sum(Inventory.quantity * Journal.debit).label("quantity"),
|
func.sum(Inventory.quantity * Journal.debit).label("quantity"),
|
||||||
func.sum(Inventory.amount * Journal.debit).label("amount"),
|
func.sum(Inventory.amount * Journal.debit).label("amount"),
|
||||||
)
|
)
|
||||||
|
.select_from(Voucher)
|
||||||
|
.join(Voucher.journals)
|
||||||
|
.join(Voucher.inventories)
|
||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
.join(Inventory.voucher)
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(Voucher.journals)
|
|
||||||
.where(
|
.where(
|
||||||
Voucher.id == Inventory.voucher_id,
|
Voucher.id == Inventory.voucher_id,
|
||||||
Voucher.id == Journal.voucher_id,
|
Voucher.id == Journal.voucher_id,
|
||||||
@@ -158,7 +190,7 @@ def opening_balance(
|
|||||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
||||||
Voucher.date_ < start_date,
|
Voucher.date_ < start_date,
|
||||||
)
|
)
|
||||||
.group_by(StockKeepingUnit.units)
|
.group_by(SkuVersion.units)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
quantity: Decimal = sum(r.quantity for r in row)
|
quantity: Decimal = sum(r.quantity for r in row)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Security
|
from fastapi import APIRouter, Request, Security
|
||||||
from sqlalchemy import select
|
from sqlalchemy import and_, or_, select
|
||||||
from sqlalchemy.orm import Session, contains_eager
|
from sqlalchemy.orm import Session, contains_eager
|
||||||
|
|
||||||
import brewman.schemas.purchase_entries as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_period
|
||||||
from ...db.session import SessionFuture
|
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]:
|
def build_report(start_date: date, finish_date: date, db: Session) -> list[schemas.PurchaseEntriesItem]:
|
||||||
body = []
|
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(
|
query: list[Voucher] = list(
|
||||||
db.execute(
|
db.execute(
|
||||||
select(Voucher)
|
select(Voucher)
|
||||||
@@ -59,12 +73,14 @@ def build_report(start_date: date, finish_date: date, db: Session) -> list[schem
|
|||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
.options(
|
.options(
|
||||||
contains_eager(Voucher.journals).contains_eager(Journal.account),
|
contains_eager(Voucher.journals).contains_eager(Journal.account),
|
||||||
contains_eager(Voucher.inventories)
|
contains_eager(Voucher.inventories)
|
||||||
.contains_eager(Inventory.batch)
|
.contains_eager(Inventory.batch)
|
||||||
.contains_eager(Batch.sku)
|
.contains_eager(Batch.sku)
|
||||||
.contains_eager(StockKeepingUnit.product),
|
.contains_eager(StockKeepingUnit.product)
|
||||||
|
.contains_eager(Product.versions),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
Voucher.date_ >= start_date,
|
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(),
|
voucher.voucher_type.name.replace("_", "-").lower(),
|
||||||
str(voucher.id),
|
str(voucher.id),
|
||||||
],
|
],
|
||||||
product=item.batch.sku.product.name,
|
product=item.batch.sku.product.versions[0].name,
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
rate=item.rate,
|
rate=item.rate,
|
||||||
tax=item.tax,
|
tax=item.tax,
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from datetime import datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Security
|
from fastapi import APIRouter, Request, Security
|
||||||
from sqlalchemy import not_
|
from sqlalchemy import and_, not_, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.sql.expression import desc, func, select
|
from sqlalchemy.sql.expression import desc, func, select
|
||||||
|
|
||||||
import brewman.schemas.purchases as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_period
|
||||||
from ...db.session import SessionFuture
|
from ...db.session import SessionFuture
|
||||||
@@ -62,25 +65,51 @@ def build_report(
|
|||||||
body = []
|
body = []
|
||||||
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
|
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")
|
amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount")
|
||||||
query = db.execute(
|
product_version_onclause = and_(
|
||||||
select(Product, quantity_sum, amount_sum)
|
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.journals)
|
||||||
.join(Voucher.inventories)
|
.join(Voucher.inventories)
|
||||||
.join(Inventory.batch)
|
.join(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
|
.join(ProductVersion.product_group)
|
||||||
.where(
|
.where(
|
||||||
Voucher.date_ >= datetime.strptime(start_date, "%d-%b-%Y"),
|
Voucher.date_ >= datetime.strptime(start_date, "%d-%b-%Y"),
|
||||||
Voucher.date_ <= datetime.strptime(finish_date, "%d-%b-%Y"),
|
Voucher.date_ <= datetime.strptime(finish_date, "%d-%b-%Y"),
|
||||||
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
|
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
|
||||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
||||||
)
|
)
|
||||||
.group_by(Product) # type: ignore
|
.group_by(ProductVersion) # type: ignore
|
||||||
.order_by(desc(amount_sum))
|
.order_by(desc(amount_sum))
|
||||||
).all()
|
).all()
|
||||||
total_amount = Decimal(0)
|
total_amount = Decimal(0)
|
||||||
for product, quantity, amount in query:
|
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
|
total_amount += amount
|
||||||
row = schemas.PurchasesItem(
|
row = schemas.PurchasesItem(
|
||||||
name=product.name,
|
name=product.name,
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ from datetime import date, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Security
|
from fastapi import APIRouter, Request, Security
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.sql.expression import case, func, select
|
from sqlalchemy.sql.expression import case, func, select
|
||||||
|
|
||||||
import brewman.schemas.raw_material_cost as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_period
|
||||||
from ...db.session import SessionFuture
|
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_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")
|
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)
|
select(CostCentre, sum_issue, sum_sale)
|
||||||
.join(CostCentre.journals)
|
.join(CostCentre.journals)
|
||||||
.join(Journal.voucher)
|
.join(Journal.voucher)
|
||||||
@@ -109,7 +113,7 @@ def build_report(
|
|||||||
for cost_centre, issue, sale in query:
|
for cost_centre, issue, sale in query:
|
||||||
issues += issue
|
issues += issue
|
||||||
sales += sale
|
sales += sale
|
||||||
rmc = 0 if sale == 0 else issue / sale
|
rmc = Decimal(0) if sale == 0 else issue / sale
|
||||||
body.append(
|
body.append(
|
||||||
schemas.RawMaterialCostItem(
|
schemas.RawMaterialCostItem(
|
||||||
name=cost_centre.name,
|
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)
|
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_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
|
||||||
sum_gross = func.sum(Inventory.amount * Journal.debit).label("gross")
|
sum_gross = func.sum(Inventory.amount * Journal.debit).label("gross")
|
||||||
|
|
||||||
query = db.execute(
|
product_version_onclause = and_(
|
||||||
select(Product, ProductGroup, sum_quantity, sum_net, sum_gross)
|
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(Inventory.batch)
|
||||||
.join(Batch.sku)
|
.join(Batch.sku)
|
||||||
|
.join(SkuVersion, onclause=sku_version_onclause)
|
||||||
.join(StockKeepingUnit.product)
|
.join(StockKeepingUnit.product)
|
||||||
.join(Product.product_group)
|
.join(ProductVersion, onclause=product_version_onclause)
|
||||||
.join(Inventory.voucher)
|
.join(ProductVersion.product_group)
|
||||||
.join(Voucher.journals)
|
|
||||||
.where(
|
.where(
|
||||||
Voucher.date_ >= start_date,
|
Voucher.date_ >= start_date,
|
||||||
Voucher.date_ <= finish_date,
|
Voucher.date_ <= finish_date,
|
||||||
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
|
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
|
||||||
Journal.cost_centre_id == cost_centre_id,
|
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())
|
.order_by(ProductGroup.name, sum_net.desc())
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from datetime import date, datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Security
|
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.orm import Session, contains_eager
|
||||||
from sqlalchemy.sql.expression import func, select
|
from sqlalchemy.sql.expression import func, select
|
||||||
|
|
||||||
import brewman.schemas.stock_movement as schemas
|
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.security import get_current_active_user as get_user
|
||||||
from ...core.session import get_finish_date, get_start_date, set_period
|
from ...core.session import get_finish_date, get_start_date, set_period
|
||||||
from ...db.session import SessionFuture
|
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]:
|
def build_stock_movement(start_date: date, finish_date: date, db: Session) -> list[schemas.StockMovementItem]:
|
||||||
dict_ = {}
|
dict_ = {}
|
||||||
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
|
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
|
||||||
openings = db.execute(
|
product_version_onclause = and_(
|
||||||
select(StockKeepingUnit, quantity_sum)
|
ProductVersion.product_id == Product.id,
|
||||||
.join(StockKeepingUnit.product)
|
or_(
|
||||||
.join(Product.product_group)
|
ProductVersion.valid_from == None, # noqa: E711
|
||||||
.join(StockKeepingUnit.batches)
|
ProductVersion.valid_from <= Voucher.date_,
|
||||||
.join(Batch.inventories)
|
),
|
||||||
.join(Inventory.voucher)
|
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)
|
.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())
|
.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()
|
).all()
|
||||||
for sku, quantity in openings:
|
for sku_version, quantity in openings:
|
||||||
dict_[sku.id] = schemas.StockMovementItem(
|
dict_[sku_version.sku_id] = schemas.StockMovementItem(
|
||||||
id_=sku.id,
|
id_=sku_version.id,
|
||||||
name=f"{sku.product.name} ({sku.units})",
|
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
|
||||||
group=sku.product.product_group.name,
|
group=sku_version.sku.product.versions[0].product_group.name,
|
||||||
opening=Decimal(round(quantity, 2)),
|
opening=Decimal(round(quantity, 2)),
|
||||||
purchase=Decimal(0),
|
purchase=Decimal(0),
|
||||||
issue=Decimal(0),
|
issue=Decimal(0),
|
||||||
closing=Decimal(0),
|
closing=Decimal(0),
|
||||||
url=["/", "product-ledger", str(sku.product.id)],
|
url=["/", "product-ledger", str(sku_version.sku.product_id)],
|
||||||
)
|
)
|
||||||
purchases = db.execute(
|
purchases: list[tuple[SkuVersion, Decimal]] = db.execute(
|
||||||
select(StockKeepingUnit, quantity_sum)
|
select(SkuVersion, quantity_sum)
|
||||||
.join(StockKeepingUnit.product)
|
.select_from(Voucher)
|
||||||
.join(Product.product_group)
|
|
||||||
.join(StockKeepingUnit.batches)
|
|
||||||
.join(Batch.inventories)
|
|
||||||
.join(Inventory.voucher)
|
|
||||||
.join(Voucher.journals)
|
.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(
|
.where(
|
||||||
Voucher.date_ >= start_date,
|
Voucher.date_ >= start_date,
|
||||||
Voucher.date_ <= finish_date,
|
Voucher.date_ <= finish_date,
|
||||||
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
|
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
|
||||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
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()
|
).all()
|
||||||
for sku, quantity in purchases:
|
for sku_version, quantity in purchases:
|
||||||
if sku.id in dict_:
|
if sku_version.sku_id in dict_:
|
||||||
dict_[sku.id].purchase = Decimal(round(quantity, 2))
|
dict_[sku_version.sku_id].purchase = Decimal(round(quantity, 2))
|
||||||
else:
|
else:
|
||||||
dict_[sku.id] = schemas.StockMovementItem(
|
dict_[sku_version.sku_id] = schemas.StockMovementItem(
|
||||||
id_=sku.id,
|
id_=sku_version.id,
|
||||||
name=f"{sku.product.name} ({sku.units})",
|
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
|
||||||
group=sku.product.product_group.name,
|
group=sku_version.sku.product.versions[0].product_group.name,
|
||||||
opening=Decimal(0),
|
opening=Decimal(0),
|
||||||
purchase=Decimal(round(quantity, 2)),
|
purchase=Decimal(round(quantity, 2)),
|
||||||
issue=Decimal(0),
|
issue=Decimal(0),
|
||||||
closing=Decimal(0),
|
closing=Decimal(0),
|
||||||
url=["/", "product-ledger", str(sku.product.id)],
|
url=["/", "product-ledger", str(sku_version.sku.product_id)],
|
||||||
)
|
)
|
||||||
issues = db.execute(
|
issues: list[tuple[SkuVersion, Decimal]] = db.execute(
|
||||||
select(StockKeepingUnit, quantity_sum)
|
select(SkuVersion, quantity_sum)
|
||||||
.join(StockKeepingUnit.product)
|
.select_from(Voucher)
|
||||||
.join(Product.product_group)
|
|
||||||
.join(StockKeepingUnit.batches)
|
|
||||||
.join(Batch.inventories)
|
|
||||||
.join(Inventory.voucher)
|
|
||||||
.join(Voucher.journals)
|
.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(
|
.where(
|
||||||
Voucher.date_ >= start_date,
|
Voucher.date_ >= start_date,
|
||||||
Voucher.date_ <= finish_date,
|
Voucher.date_ <= finish_date,
|
||||||
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
|
Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK]),
|
||||||
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
|
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()
|
).all()
|
||||||
for sku, quantity in issues:
|
for sku_version, quantity in issues:
|
||||||
if sku.id in dict_:
|
if sku_version.sku_id in dict_:
|
||||||
dict_[sku.id].issue = Decimal(round(quantity * -1, 2))
|
dict_[sku_version.sku_id].issue = Decimal(round(quantity * -1, 2))
|
||||||
else:
|
else:
|
||||||
dict_[sku.id] = schemas.StockMovementItem(
|
dict_[sku_version.sku_id] = schemas.StockMovementItem(
|
||||||
id_=sku.id,
|
id_=sku_version.id,
|
||||||
name=f"{sku.product.name} ({sku.units})",
|
name=f"{sku_version.sku.product.versions[0].name} ({sku_version.units})",
|
||||||
group=sku.product.product_group.name,
|
group=sku_version.sku.product.versions[0].product_group.name,
|
||||||
opening=Decimal(0),
|
opening=Decimal(0),
|
||||||
purchase=Decimal(0),
|
purchase=Decimal(0),
|
||||||
issue=Decimal(round(quantity * -1, 2)),
|
issue=Decimal(round(quantity * -1, 2)),
|
||||||
closing=Decimal(0),
|
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()]
|
list_ = [value for key, value in dict_.items()]
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
import brewman.schemas.voucher as output
|
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.security import get_current_active_user as get_user
|
||||||
from ..core.session import get_first_day
|
from ..core.session import get_first_day
|
||||||
from ..db.session import SessionFuture
|
from ..db.session import SessionFuture
|
||||||
@@ -137,10 +140,26 @@ def delete_voucher(
|
|||||||
else:
|
else:
|
||||||
for i_item in voucher.inventories:
|
for i_item in voucher.inventories:
|
||||||
if i_item.batch.quantity_remaining < i_item.quantity:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_423_LOCKED,
|
status_code=status.HTTP_423_LOCKED,
|
||||||
detail=f"Only {i_item.batch.quantity_remaining}"
|
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",
|
f"So it cannot be deleted",
|
||||||
)
|
)
|
||||||
i_item.batch.quantity_remaining -= i_item.quantity
|
i_item.batch.quantity_remaining -= i_item.quantity
|
||||||
@@ -152,9 +171,24 @@ def delete_voucher(
|
|||||||
)
|
)
|
||||||
).scalar_one()
|
).scalar_one()
|
||||||
if uses > 0:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_423_LOCKED,
|
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)
|
batches_to_delete.append(i_item.batch)
|
||||||
elif voucher.voucher_type == VoucherType.PURCHASE_RETURN:
|
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:
|
if len(json_voucher.incentives) > 0:
|
||||||
json_voucher.incentive = next(x.amount for x in voucher.journals if x.account_id == Account.incentive_id())
|
json_voucher.incentive = next(x.amount for x in voucher.journals if x.account_id == Account.incentive_id())
|
||||||
for inventory in voucher.inventories:
|
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 = (
|
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"{inventory.batch.quantity_remaining:.2f}@{inventory.batch.rate:.2f} "
|
||||||
f"from {inventory.batch.name.strftime('%d-%b-%Y')}"
|
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,
|
rate=inventory.batch.rate,
|
||||||
sku=output.ProductLink(
|
sku=output.ProductLink(
|
||||||
id_=inventory.batch.sku.id,
|
id_=inventory.batch.sku.id,
|
||||||
name=f"{inventory.batch.sku.product.name} ({inventory.batch.sku.units})"
|
name=f"{product_name} ({product_unit})" if product_unit else product_name,
|
||||||
if inventory.batch.sku.units
|
|
||||||
else inventory.batch.sku.product.name,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class MozimoDailyRegisterItem(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class MozimoDailyRegister(BaseModel):
|
class MozimoDailyRegister(BaseModel):
|
||||||
date_: date
|
date_: date | str
|
||||||
body: list[MozimoDailyRegisterItem]
|
body: list[MozimoDailyRegisterItem]
|
||||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
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):
|
class MozimoProductRegister(BaseModel):
|
||||||
start_date: date | None = None
|
start_date: date | str
|
||||||
finish_date: date | None = None
|
finish_date: date | str
|
||||||
product: ProductLink | None = None
|
product: ProductLink | None = None
|
||||||
body: list[MozimoProductRegisterItem]
|
body: list[MozimoProductRegisterItem]
|
||||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
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):
|
class Product(ProductIn):
|
||||||
id_: uuid.UUID
|
id_: uuid.UUID
|
||||||
code: int
|
handle: str
|
||||||
is_fixture: bool
|
is_fixture: bool
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from . import Daf, to_camel
|
from . import Daf, to_camel
|
||||||
@@ -8,8 +10,8 @@ from . import Daf, to_camel
|
|||||||
class StockKeepingUnit(BaseModel):
|
class StockKeepingUnit(BaseModel):
|
||||||
id_: uuid.UUID | None = None
|
id_: uuid.UUID | None = None
|
||||||
units: str = Field(..., min_length=1)
|
units: str = Field(..., min_length=1)
|
||||||
fraction: Daf = Field(ge=1, default=1)
|
fraction: Daf = Field(ge=Decimal(1), default=Decimal(1))
|
||||||
product_yield: Daf = Field(gt=0, le=1, default=1)
|
product_yield: Daf = Field(gt=Decimal(0), le=Decimal(1), default=Decimal(1))
|
||||||
cost_price: Daf = Field(ge=0, default=0)
|
cost_price: Daf = Field(ge=Decimal(0), default=Decimal(0))
|
||||||
sale_price: Daf = Field(ge=0, default=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)
|
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
|
./version_bump.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download the package.json for caching
|
make build-production
|
||||||
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 .
|
|
||||||
if [ 1 -eq "$#" ]
|
if [ 1 -eq "$#" ]
|
||||||
then
|
then
|
||||||
docker tag brewman:latest "$1"
|
docker tag brewman:latest "$1"
|
||||||
else
|
else
|
||||||
echo "No version bump"
|
echo "No version bump"
|
||||||
fi
|
fi
|
||||||
cd "$parent_path/docker" || exit
|
cd "$parent_path/ansible" || exit
|
||||||
docker save brewman:latest | bzip2 | pv | ssh gondor 'bunzip2 | sudo docker load'
|
|
||||||
ansible-playbook --inventory hosts playbook.yml
|
ansible-playbook --inventory hosts playbook.yml
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
|
||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete';
|
|
||||||
import { MatButton } from '@angular/material/button';
|
import { MatButton } from '@angular/material/button';
|
||||||
import { MatOption } from '@angular/material/core';
|
|
||||||
import { MatDatepicker } from '@angular/material/datepicker';
|
|
||||||
import {
|
import {
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
@@ -14,12 +10,10 @@ import {
|
|||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
MatDialogClose,
|
MatDialogClose,
|
||||||
} from '@angular/material/dialog';
|
} 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 { MatInput } from '@angular/material/input';
|
||||||
import { MatSelect } from '@angular/material/select';
|
|
||||||
|
|
||||||
import { Tag } from '../tag/tag';
|
import { Tag } from '../tag/tag';
|
||||||
import { AccountingPipe } from '../shared/accounting.pipe';
|
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { LedgerItem } from '../ledger/ledger-item';
|
import { LedgerItem } from '../ledger/ledger-item';
|
||||||
import { TagList } from './tag-list';
|
import { TagList } from './tag-list';
|
||||||
@@ -32,24 +26,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|||||||
styleUrls: ['./tag-dialog.component.css'],
|
styleUrls: ['./tag-dialog.component.css'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
AccountingPipe,
|
|
||||||
AsyncPipe,
|
|
||||||
CurrencyPipe,
|
|
||||||
CdkScrollable,
|
CdkScrollable,
|
||||||
MatCheckbox,
|
MatCheckbox,
|
||||||
MatDialogTitle,
|
MatDialogTitle,
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatFormField,
|
MatFormField,
|
||||||
MatSelect,
|
|
||||||
MatDatepicker,
|
|
||||||
MatOption,
|
|
||||||
MatLabel,
|
MatLabel,
|
||||||
MatInput,
|
MatInput,
|
||||||
MatAutocompleteTrigger,
|
|
||||||
MatHint,
|
|
||||||
MatAutocomplete,
|
|
||||||
MatPrefix,
|
|
||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
MatButton,
|
MatButton,
|
||||||
MatDialogClose,
|
MatDialogClose,
|
||||||
|
|||||||
Reference in new issue
Block a user