Moved to uv from poetry

Updated ruff and mypy, accepted all changes
Central exception management and injected the Session.
This removed a lot of duplicated boilerplate code.
Added health check
Updated to Angular 21
This commit is contained in:
tanshu committed 2026-02-24 03:08:05 +00:00
1 parent 770fd0c6dd
commit 1e7476c5d9
148 files changed
+7600 -11635

No files matched your search

+3 -3
View File
@@ -1,6 +1,6 @@
{
// Use WSL paths + Poetry venv interpreter
"python.defaultInterpreterPath": "/home/tanshu/.cache/pypoetry/virtualenvs/brewman-C3_Ccu4O-py3.14/bin/python",
"python.defaultInterpreterPath": "${workspaceFolder}/brewman/.venv/bin/python",
"python.terminal.activateEnvironment": true,
// Quality-of-life
/* --- Pylance: keep IntelliSense, disable type checking --- */
@@ -45,7 +45,7 @@
"source.fixAll.ruff": "always",
"source.fixAll.eslint": "always",
},
"python-envs.defaultEnvManager": "ms-python.python:poetry",
"python-envs.defaultPackageManager": "ms-python.python:poetry",
"python-envs.defaultEnvManager": "ms-python.python:venv",
"python-envs.defaultPackageManager": "ms-python.python:pip",
"python-envs.pythonProjects": []
}
+55 -20
View File
@@ -1,9 +1,13 @@
FROM node:latest AS base
FROM node:lts-trixie-slim AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies based on the preferred package manager
COPY overlord/package.json overlord/yarn.lock* overlord/package-lock.json* overlord/pnpm-lock.yaml* ./
RUN \
@@ -26,39 +30,70 @@ RUN \
fi
FROM python:3.13 AS runner
FROM python:3.14-slim 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
RUN apt update \
&& apt install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* \
apt update && \
apt install -y locales && \
sed --in-place --expression='s/# en_IN UTF-8/en_IN UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales \
&& rm -rf /var/lib/apt/lists/*
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
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1
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"
# Disable development dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-a-project
ENV UV_NO_DEV=1
# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy
WORKDIR /app/
# Place executables in the environment at the front of the path
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
ENV PATH="/app/.venv/bin:$PATH"
# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=brewman/uv.lock,target=uv.lock \
--mount=type=bind,source=brewman/pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project
COPY /brewman ./
COPY --from=builder /frontend/browser /app/static
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=brtewman/uv.lock,target=uv.lock \
--mount=type=bind,source=brewman/pyproject.toml,target=pyproject.toml \
uv sync --locked
ENV PYTHONPATH=/app
EXPOSE 80
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 /
&& 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"]
# at the end of your Dockerfile, before CMD or after EXPOSE
# Kill the main process if the healthcheck fails. This will kill the container and restart policy can restart it.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://localhost/health || kill -s 15 1
CMD ["gunicorn", "brtewman.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--config", "./gunicorn.conf.py", "--log-config", "./logging.conf"]
+19
View File
@@ -6,3 +6,22 @@ build-production: ## Build the production docker image.
$(if $(TAG),--tag registry.tanshu.com/brewman:$(TAG)) \
--push \
git@git.tanshu.com:tanshu/brewman.git
.PHONY: build-check
build-check: ## Multi-arch build without push (compile check)
@docker buildx build \
--platform linux/amd64,linux/arm64/v8 \
--tag brewman:test \
--pull \
--progress=plain \
git@git.tanshu.com:tanshu/barker.git
.PHONY: build-check-local
build-check-local: ## Multi-arch build without push (compile check)
@git archive --format=tar HEAD | docker buildx build \
--platform linux/amd64 \
--tag brewman:test \
--pull \
--progress=plain \
--load \
-
+106 -32
View File
@@ -1,45 +1,119 @@
## Prettier - to format typescript files
```npx prettier --write src/app/```
# Installation linux
## Eslint - to lint typescript files
```shell script
eslint src/app/
```
Optional to fix the errors as well
```
eslint src/app/ --fix
This project uses **uv** for Python version, dependency and virtual environment management.
---
## 1. Install uv
### 1.1 Install uv (if not already installed)
```zsh
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### Poerty remove environment
```sh
rm -rf `poetry env info -p`
Reload the shell:
```zsh
exec "$SHELL"
```
### Poerty install
```sh
poetry install
Verify:
```zsh
uv --version
```
## 2. Install Python 3.14 using uv
```zsh
uv python install 3.14
```
### Setup python environment
```sh
# Install pyenv to manage python versions
curl https://pyenv.run | bash
Set the local Python version for this project:
# 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
```zsh
uv python pin 3.14
```
# Install python 3.13
pyenv install 3.13
Verify:
# Use python in the ~/programming/brewman/brewman directory
cd ~/programming/brewman/brewman
pyenv local 3.13
```zsh
python --version
```
# Tell Poetry to use the pyenv Python
poetry env use python
---
# Or this
eval $(poetry env activate)
## 3. Install project dependencies
### 3.1 Install dependencies
```zsh
uv sync
```
This will:
* Create a local `.venv`
* Install all dependencies defined in `pyproject.toml`
* Generate/update `uv.lock`
* Use Python **3.14**
---
## 4. Activate the environment (optional)
```zsh
source .venv/bin/activate
```
Deactivate:
```zsh
deactivate
```
Or run commands directly:
```zsh
uv run python your_script.py
```
---
## 5. Install nvm
### 5.1 Install nvm (if not already installed)
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
```
### 5.2 Configure shell for pyenv
Add the following to `~/.zshrc`:
```zsh
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
```
Reload the shell:
```zsh
exec "$SHELL"
```
Verify:
```zsh
nvm --version
```
---
## Notes
* Do **not** use system-wide pip for this project.
* Do **not** install dependencies manually.
* Always use `uv sync` to install or update dependencies.
* Commit both: `pyproject.toml` and `uv.lock`
* PostgreSQL must be running and properly configured before application startup.
+5 -4
View File
@@ -3,20 +3,21 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.6
rev: v0.15.2
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
args:
- --fix
- --config=barker/pyproject.toml
# Run the formatter.
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: debug-statements
+1 -1
View File
@@ -62,7 +62,7 @@ def client_allowed(user: UserModel, client_id: int, otp: int | None, db: Session
)
if client is None:
client = Client.create(db)
allowed = "clients" in set([p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions])
allowed = "clients" in {p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions}
if not client.enabled and otp is not None and client.otp == otp:
client.enabled = True
client.otp = None
-42
View File
@@ -1,42 +0,0 @@
from datetime import date, timedelta
from typing import Any
def get_date(session: dict[str, Any]) -> str:
if "date" not in session:
session["date"] = date.today().strftime("%d-%b-%Y")
return session["date"] # type: ignore[no-any-return]
def set_date(date_: str | date, session: dict[str, Any]) -> None:
session["date"] = date_ if isinstance(date_, str) else date_.strftime("%d-%b-%Y")
def get_start_date(session: dict[str, Any]) -> str:
if "start" not in session:
session["start"] = get_first_day(date.today()).strftime("%d-%b-%Y")
return session["start"] # type: ignore[no-any-return]
def get_finish_date(session: dict[str, Any]) -> str:
if "finish" not in session:
session["finish"] = get_last_day(date.today()).strftime("%d-%b-%Y")
return session["finish"] # type: ignore[no-any-return]
def set_period(start: str | date | None, finish: str | date | None, session: dict[str, Any]) -> None:
if start is not None:
session["start"] = start if isinstance(start, str) else start.strftime("%d-%b-%Y")
if finish is not None:
session["finish"] = finish if isinstance(finish, str) else finish.strftime("%d-%b-%Y")
def get_first_day(dt: date, d_years: int = 0, d_months: int = 0) -> date:
# d_years, d_months are "deltas" to apply to dt
y, m = dt.year + d_years, dt.month + d_months
a, m = divmod(m - 1, 12)
return date(y + a, m + 1, 1)
def get_last_day(dt: date) -> date:
return get_first_day(dt, 0, 1) + timedelta(-1)
+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from sqlalchemy.exc import IntegrityError
@dataclass(frozen=True)
class FriendlyDBError:
status: int
code: str
detail: str
constraint: str | None = None
# SQLSTATE → default mapping (Postgres)
_SQLSTATE_DEFAULTS: dict[str, tuple[int, str, str]] = {
"23505": (409, "UNIQUE_VIOLATION", "Already exists."),
"23503": (409, "FOREIGN_KEY_VIOLATION", "Referenced record not found."),
"23514": (400, "CHECK_VIOLATION", "Invalid value."),
"23502": (400, "NOT_NULL_VIOLATION", "Missing required value."),
}
# Constraint-name → friendly mapping for your schema
# Keep this as the "source of truth" for frontend-friendly messages.
_CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = {
# --- customers ---
"uq_customers_phone": FriendlyDBError(
409, "CUSTOMER_PHONE_EXISTS", "A customer with this phone number already exists."
),
"ck_customers_phone_not_blank": FriendlyDBError(400, "CUSTOMER_PHONE_BLANK", "Phone number cannot be blank."),
"ck_customers_name_not_blank": FriendlyDBError(400, "CUSTOMER_NAME_BLANK", "Customer name cannot be blank."),
# --- devices / tables / sections / printers / settings ---
"uq_devices_name": FriendlyDBError(409, "DEVICE_NAME_EXISTS", "A device with this name already exists."),
"uq_food_tables_name": FriendlyDBError(409, "FOOD_TABLE_NAME_EXISTS", "A table with this name already exists."),
"uq_sections_name": FriendlyDBError(409, "SECTION_NAME_EXISTS", "A section with this name already exists."),
"uq_printers_name": FriendlyDBError(409, "PRINTER_NAME_EXISTS", "A printer with this name already exists."),
"uq_settings_name": FriendlyDBError(409, "SETTING_NAME_EXISTS", "A setting with this name already exists."),
# --- regimes / taxes / sale_categories ---
"uq_regimes_name": FriendlyDBError(409, "REGIME_NAME_EXISTS", "A regime with this name already exists."),
"uq_regimes_prefix": FriendlyDBError(409, "REGIME_PREFIX_EXISTS", "A regime with this prefix already exists."),
"uq_taxes_name": FriendlyDBError(409, "TAX_NAME_EXISTS", "A tax with this name already exists."),
"uq_sale_categories_name": FriendlyDBError(
409, "SALE_CATEGORY_NAME_EXISTS", "A sale category with this name already exists."
),
# --- roles / permissions / settle_options ---
"uq_roles_name": FriendlyDBError(409, "ROLE_NAME_EXISTS", "A role with this name already exists."),
"uq_permissions_name": FriendlyDBError(
409, "PERMISSION_NAME_EXISTS", "A permission with this name already exists."
),
"uq_settle_options_name": FriendlyDBError(
409, "SETTLE_OPTION_NAME_EXISTS", "A settle option with this name already exists."
),
# --- kots / vouchers / bills / overview / settlements / reprints ---
"uq_kots_code": FriendlyDBError(409, "KOT_CODE_EXISTS", "This KOT code already exists."),
"uq_vouchers_kot_id": FriendlyDBError(409, "VOUCHER_FOR_KOT_EXISTS", "A voucher already exists for this KOT."),
"uq_bills_voucher_id": FriendlyDBError(409, "BILL_FOR_VOUCHER_EXISTS", "A bill already exists for this voucher."),
"uq_bills_regime_id": FriendlyDBError(409, "BILL_REGIME_CONFLICT", "Bill regime conflicts with an existing bill."),
"uq_overview_voucher_id": FriendlyDBError(
409, "OVERVIEW_FOR_VOUCHER_EXISTS", "An overview already exists for this voucher."
),
"uq_overview_food_table_id": FriendlyDBError(
409, "OVERVIEW_FOR_TABLE_EXISTS", "An overview already exists for this table."
),
"uq_overview_guest_book_id": FriendlyDBError(
409, "OVERVIEW_FOR_GUEST_EXISTS", "An overview already exists for this guest entry."
),
"uq_settlements_voucher_id": FriendlyDBError(
409, "SETTLEMENT_FOR_VOUCHER_EXISTS", "A settlement already exists for this voucher."
),
# --- login_history / user_roles / role_includes / role_permissions ---
"uq_login_history_user_id": FriendlyDBError(
409, "LOGIN_HISTORY_EXISTS", "Login history already exists for this user."
),
"uq_user_roles_user_id": FriendlyDBError(
409, "USER_ROLE_EXISTS", "A role assignment already exists for this user."
),
"uq_role_includes_role_id": FriendlyDBError(
409, "ROLE_INCLUDE_EXISTS", "This role-include relation already exists."
),
"uq_role_permissions_permission_id": FriendlyDBError(
409, "ROLE_PERMISSION_EXISTS", "This permission is already assigned."
),
# --- customer_discount ---
"uq_customer_discount_customer_id": FriendlyDBError(
409, "CUSTOMER_DISCOUNT_EXISTS", "Discount is already configured for this customer."
),
# --- inventory_modifiers / inventories ---
"uq_inventory_modifiers_inventory_id": FriendlyDBError(
409, "INVENTORY_MODIFIERS_EXISTS", "Modifiers already exist for this inventory item."
),
"uq_inventories_kot_id": FriendlyDBError(
409, "INVENTORY_FOR_KOT_EXISTS", "Inventory entry already exists for this KOT."
),
# --- menu_categories / modifier_categories / modifiers ---
"uq_menu_categories_name": FriendlyDBError(
409, "MENU_CATEGORY_NAME_EXISTS", "A menu category with this name already exists."
),
"uq_modifier_categories_name": FriendlyDBError(
409, "MODIFIER_CATEGORY_NAME_EXISTS", "A modifier category with this name already exists."
),
"uq_modifiers_name": FriendlyDBError(409, "MODIFIER_NAME_EXISTS", "A modifier with this name already exists."),
# --- exclude constraints (time-range overlaps / duplicates) ---
"uq_bundle_items_bundle_id_item_id": FriendlyDBError(
409, "BUNDLE_ITEM_EXISTS", "This item is already in the bundle."
),
"uq_product_versions_name": FriendlyDBError(
409,
"PRODUCT_VERSION_NAME_OVERLAP",
"Product name overlaps with an existing version in the selected date range.",
),
"uq_product_versions_product_id": FriendlyDBError(
409, "PRODUCT_VERSION_OVERLAP", "Product version overlaps with an existing version in the selected date range."
),
"uq_sku_versions_product_id_units": FriendlyDBError(
409, "SKU_PRODUCT_UNITS_OVERLAP", "This unit already exists for the product in the selected date range."
),
"uq_sku_versions_sku_id_units": FriendlyDBError(
409, "SKU_UNITS_OVERLAP", "This unit already exists for the SKU in the selected date range."
),
"uq_sku_versions_sku_id": FriendlyDBError(
409, "SKU_VERSION_OVERLAP", "SKU version overlaps with an existing version in the selected date range."
),
}
# --- UNIQUE INDEXES (not constraints) ---
_UNIQUE_INDEX_FRIENDLY: dict[str, FriendlyDBError] = {
"uq_users_name": FriendlyDBError(
409,
"USER_NAME_EXISTS",
"A user with this name already exists.",
),
}
# Optional: FK constraint names → friendlier messages (instead of generic “referenced record not found”)
# Add only the ones where you want specific UX.
_FK_FRIENDLY: dict[str, FriendlyDBError] = {
"fk_guest_book_customer_id_customers": FriendlyDBError(409, "CUSTOMER_NOT_FOUND", "Customer does not exist."),
"fk_vouchers_customer_id_customers": FriendlyDBError(409, "CUSTOMER_NOT_FOUND", "Customer does not exist."),
"fk_vouchers_food_table_id_food_tables": FriendlyDBError(409, "FOOD_TABLE_NOT_FOUND", "Food table does not exist."),
"fk_kots_food_table_id_food_tables": FriendlyDBError(409, "FOOD_TABLE_NOT_FOUND", "Food table does not exist."),
"fk_kots_user_id_users": FriendlyDBError(409, "USER_NOT_FOUND", "User does not exist."),
"fk_vouchers_user_id_users": FriendlyDBError(409, "USER_NOT_FOUND", "User does not exist."),
"fk_devices_section_id_sections": FriendlyDBError(409, "SECTION_NOT_FOUND", "Section does not exist."),
"fk_food_tables_section_id_sections": FriendlyDBError(409, "SECTION_NOT_FOUND", "Section does not exist."),
"fk_sku_versions_menu_category_id_menu_categories": FriendlyDBError(
409, "MENU_CATEGORY_NOT_FOUND", "Menu category does not exist."
),
"fk_stock_keeping_units_product_id_products": FriendlyDBError(409, "PRODUCT_NOT_FOUND", "Product does not exist."),
"fk_product_versions_product_id_products": FriendlyDBError(409, "PRODUCT_NOT_FOUND", "Product does not exist."),
"fk_products_sale_category_id_sale_categories": FriendlyDBError(
409, "SALE_CATEGORY_NOT_FOUND", "Sale category does not exist."
),
"fk_customer_discount_sale_category_id_sale_categories": FriendlyDBError(
409, "SALE_CATEGORY_NOT_FOUND", "Sale category does not exist."
),
"fk_sale_categories_tax_id_taxes": FriendlyDBError(409, "TAX_NOT_FOUND", "Tax does not exist."),
"fk_taxes_regime_id_regimes": FriendlyDBError(409, "REGIME_NOT_FOUND", "Regime does not exist."),
}
def _psycopg3_sqlstate(exc: IntegrityError) -> str | None:
orig = getattr(exc, "orig", None)
return getattr(orig, "sqlstate", None) if orig else None
def _psycopg3_constraint_name(exc: IntegrityError) -> str | None:
orig = getattr(exc, "orig", None)
diag = getattr(orig, "diag", None) if orig else None
return getattr(diag, "constraint_name", None) if diag else None
def constraint_to_friendly(exc: IntegrityError) -> FriendlyDBError:
"""
Convert a SQLAlchemy IntegrityError (psycopg3 / Postgres) to a frontend-friendly error.
Precedence:
A) Exact constraint-name mapping (_CONSTRAINT_FRIENDLY)
B) FK-specific mapping (_FK_FRIENDLY)
C) SQLSTATE generic mapping
D) Safe fallback
"""
sqlstate = _psycopg3_sqlstate(exc)
constraint = _psycopg3_constraint_name(exc)
if constraint:
if constraint in _CONSTRAINT_FRIENDLY:
e = _CONSTRAINT_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
if constraint in _UNIQUE_INDEX_FRIENDLY:
e = _UNIQUE_INDEX_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
if constraint in _FK_FRIENDLY:
e = _FK_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
# common pattern: *_not_null constraints → 400 with field hint
if constraint.endswith("_not_null"):
# e.g. "customers_phone_not_null" → "phone"
field = re.sub(r"^.*?_", "", constraint).replace("_not_null", "")
return FriendlyDBError(
400,
"REQUIRED_FIELD_MISSING",
f"'{field}' is required.",
constraint=constraint,
)
if sqlstate is None:
status, code, detail = (409, "INTEGRITY_ERROR", "Conflict.")
else:
status, code, detail = _SQLSTATE_DEFAULTS.get(sqlstate, (409, "INTEGRITY_ERROR", "Conflict."))
return FriendlyDBError(status, code, detail, constraint=constraint)
+17 -1
View File
@@ -1,7 +1,11 @@
import logging
from collections.abc import Generator
from typing import Annotated
from fastapi import Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import Session, sessionmaker
from ..core.config import settings
@@ -10,3 +14,15 @@ logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(settings.LOG_LEVEL)
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True, future=True)
SessionFuture = sessionmaker(autoflush=False, bind=engine, future=True)
def get_session() -> Generator[Session]:
with SessionFuture() as session:
try:
yield session
except Exception:
session.rollback()
raise
SessionDep = Annotated[Session, Depends(get_session)]
+30 -1
View File
@@ -1,11 +1,16 @@
import logging
import uvicorn
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.middleware.sessions import SessionMiddleware
from .core.config import settings
from .db.base import reg # noqa: F401
from .db.friendly_db_error import constraint_to_friendly
from .routers import (
account,
account_types,
@@ -24,6 +29,7 @@ from .routers import (
employee_benefit,
fingerprint,
fingerprint_report,
health,
incentive,
issue,
issue_grid,
@@ -69,9 +75,31 @@ from .routers.reports import (
)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.exception_handler(SQLAlchemyError)
async def sqlalchemy_error_handler(_request: Request, exc: SQLAlchemyError) -> JSONResponse:
logger.exception("Database error", exc_info=exc) # log full details server-side
return JSONResponse(
status_code=500,
content={
"detail": "Database error\n" + str(exc),
"code": "DB_ERROR",
},
)
@app.exception_handler(IntegrityError)
async def integrity_error_handler(_request: Request, exc: IntegrityError) -> JSONResponse:
err = constraint_to_friendly(exc)
return JSONResponse(
status_code=err.status,
content={"detail": err.detail, "code": err.code, "constraint": err.constraint},
)
app.add_middleware(SessionMiddleware, secret_key=settings.MIDDLEWARE_SECRET_KEY)
app.include_router(db_image.router, prefix="/db-image", tags=["db-image"])
app.include_router(login.router, tags=["login"])
@@ -141,6 +169,7 @@ app.include_router(db_integrity.router, prefix="/api/db-integrity", tags=["manag
app.include_router(rebase.router, prefix="/api/rebase", tags=["management"])
app.include_router(title.router, prefix="/api/title")
app.include_router(health.router, prefix="/health", tags=["health"])
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -1,3 +1,5 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import uuid
from decimal import Decimal
+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import uuid
from decimal import Decimal
+2 -2
View File
@@ -45,9 +45,9 @@ class Price:
self.period_id = period_id
if product_id is not None:
self.product_id = product_id
if period is not None and (period.id is not None or period_id is None):
if period is not None:
self.period = period
if product is not None and (product.id is not None or product_id is None):
if product is not None:
self.product = product
if id_ is not None:
self.id = id_
+3 -3
View File
@@ -19,7 +19,7 @@ def check_journals_are_valid(voucher: Voucher) -> None:
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Journal amounts cannot be negative",
)
journal_set = set(hash(x.account_id) ^ hash(x.cost_centre_id) for x in voucher.journals)
journal_set = {hash(x.account_id) ^ hash(x.cost_centre_id) for x in voucher.journals}
if len(voucher.journals) != len(journal_set):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -33,7 +33,7 @@ def check_duplicate_batches(voucher: Voucher) -> None:
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough inventories",
)
product_set = set(x.batch.id for x in voucher.inventories)
product_set = {x.batch.id for x in voucher.inventories}
if len(voucher.inventories) != len(product_set):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -47,7 +47,7 @@ def check_duplicate_skus(voucher: Voucher) -> None:
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough inventories",
)
product_set = set(x.batch.sku_id for x in voucher.inventories)
product_set = {x.batch.sku_id for x in voucher.inventories}
if len(voucher.inventories) != len(product_set):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+6 -15
View File
@@ -2,11 +2,9 @@ from collections.abc import Sequence
from datetime import UTC, date, datetime, timedelta
from typing import Any
from fastapi import Request
from sqlalchemy import ColumnElement, Label, and_, or_, select
from sqlalchemy.orm import Session
from sqlalchemy.orm import InstrumentedAttribute, Session
from ..core.session import get_date
from ..models.db_setting import DbSetting
from ..models.product import Product
from ..models.product_version import ProductVersion
@@ -57,7 +55,7 @@ def get_lock_info(
# Data format
if len(li.voucher_types) != 0 and voucher_type not in [vt.id_ for vt in li.voucher_types]:
continue
if len(li.account_types) != 0 and len(set(account_types) & set([at.id_ for at in li.account_types])) == 0:
if len(li.account_types) != 0 and len(set(account_types) & {at.id_ for at in li.account_types}) == 0:
continue
if li.start.days is not None:
@@ -92,28 +90,21 @@ def effective_date(d: str | None = None) -> date:
)
def session_or_query_date(request: Request, d: str | None = None) -> date:
if d:
return datetime.strptime(d, "%d-%b-%Y").date()
session_date_str = get_date(request.session)
return datetime.strptime(session_date_str, "%d-%b-%Y").date()
def _pv_active(date_: date | Label[Any]) -> ColumnElement[bool]:
def _pv_active(date_: date | Label[Any] | InstrumentedAttribute[date]) -> ColumnElement[bool]:
return and_(
or_(ProductVersion.valid_from == None, ProductVersion.valid_from <= date_), # noqa: E711
or_(ProductVersion.valid_till == None, ProductVersion.valid_till >= date_), # noqa: E711
)
def _sv_active(date_: date | Label[Any]) -> ColumnElement[bool]:
def _sv_active(date_: date | Label[Any] | InstrumentedAttribute[date]) -> ColumnElement[bool]:
return and_(
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
def _pv_onclause(date_: date | Label[Any]) -> ColumnElement[bool]:
def _pv_onclause(date_: date | Label[Any] | InstrumentedAttribute[date]) -> ColumnElement[bool]:
return and_(
ProductVersion.product_id == Product.id,
or_(
@@ -127,7 +118,7 @@ def _pv_onclause(date_: date | Label[Any]) -> ColumnElement[bool]:
)
def _sv_onclause(date_: date | Label[Any]) -> ColumnElement[bool]:
def _sv_onclause(date_: date | Label[Any] | InstrumentedAttribute[date]) -> ColumnElement[bool]:
return and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
+78 -97
View File
@@ -2,14 +2,14 @@ import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, joinedload
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.account_base import AccountBase
from ..models.account_type import AccountType
@@ -29,150 +29,131 @@ router = APIRouter()
@router.post("", response_model=None)
def save(
data: schemas.AccountIn,
user: UserToken = Security(get_user, scopes=["accounts"]),
data: schemas.AccountIn, user: Annotated[UserToken, Security(get_user, scopes=["accounts"])], db: SessionDep
) -> None:
try:
with SessionFuture() as db:
item = Account(
name=data.name,
code=Account.get_code(data.type_, db),
type_id=data.type_,
is_starred=data.is_starred,
is_active=data.is_active,
is_reconcilable=data.is_reconcilable,
cost_centre_id=data.cost_centre.id_,
)
item = Account(
name=data.name,
code=Account.get_code(data.type_, db),
type_id=data.type_,
is_starred=data.is_starred,
is_active=data.is_active,
is_reconcilable=data.is_reconcilable,
cost_centre_id=data.cost_centre.id_,
)
db.add(item)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
db.add(item)
db.commit()
@router.put("/{id_}", response_model=None)
def update_route(
id_: uuid.UUID,
data: schemas.AccountIn,
user: UserToken = Security(get_user, scopes=["accounts"]),
user: Annotated[UserToken, Security(get_user, scopes=["accounts"])],
db: SessionDep,
) -> None:
try:
with SessionFuture() as db:
item: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
if item.type_id != data.type_:
item.code = Account.get_code(data.type_, db)
item.type_id = data.type_
item.name = data.name
item.is_active = data.is_active
item.is_reconcilable = data.is_reconcilable
item.is_starred = data.is_starred
item.cost_centre_id = data.cost_centre.id_
db.commit()
except SQLAlchemyError as e:
item: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
if item.type_id != data.type_:
item.code = Account.get_code(data.type_, db)
item.type_id = data.type_
item.name = data.name
item.is_active = data.is_active
item.is_reconcilable = data.is_reconcilable
item.is_starred = data.is_starred
item.cost_centre_id = data.cost_centre.id_
db.commit()
@router.delete("/{id_}", response_model=schemas.AccountBlank)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["accounts"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["accounts"])], db: SessionDep
) -> schemas.AccountBlank:
with SessionFuture() as db:
account: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
can_delete, reason = account.can_delete("advanced-delete" in user.permissions)
if can_delete:
delete_with_data(account, db)
db.commit()
return account_blank(db)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Cannot delete account because {reason}",
)
account: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
can_delete, reason = account.can_delete("advanced-delete" in user.permissions)
if can_delete:
delete_with_data(account, db)
db.commit()
return account_blank(db)
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Cannot delete account because {reason}",
)
@router.get("", response_model=schemas.AccountBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["accounts"]),
user: Annotated[UserToken, Security(get_user, scopes=["accounts"])], db: SessionDep
) -> schemas.AccountBlank:
with SessionFuture() as db:
return account_blank(db)
return account_blank(db)
@router.get("/list", response_model=list[schemas.Account])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Account]:
with SessionFuture() as db:
return [
account_info(item)
for item in db.execute(
select(Account).order_by(Account.type_id).order_by(Account.name).order_by(Account.code)
)
.scalars()
.all()
]
async def show_list(
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
) -> list[schemas.Account]:
return [
account_info(item)
for item in db.execute(select(Account).order_by(Account.type_id).order_by(Account.name).order_by(Account.code))
.scalars()
.all()
]
@router.get("/query", response_model=list[AccountLink])
async def show_term(
q: str,
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
t: int | None = None, # AccountType
r: bool | None = None, # Reconcilable
a: bool | None = None, # Active
c: int | None = None, # Count
current_user: UserToken = Depends(get_user),
) -> list[AccountLink]:
list_: list[AccountLink] = []
with SessionFuture() as db:
query_ = select(AccountBase)
if t is not None:
query_ = query_.where(AccountBase.type_id == t)
if r is not None:
query_ = query_.where(AccountBase.is_reconcilable == r)
if a is not None:
query_ = query_.where(AccountBase.is_active == a)
if q is not None:
for name in q.split():
query_ = query_.where(AccountBase.name.ilike(f"%{name}%"))
query_ = query_.order_by(AccountBase.name)
if c is not None:
query_ = query_.limit(c)
data: list[AccountBase] = list(db.execute(query_).scalars().all())
query_ = select(AccountBase)
if t is not None:
query_ = query_.where(AccountBase.type_id == t)
if r is not None:
query_ = query_.where(AccountBase.is_reconcilable == r)
if a is not None:
query_ = query_.where(AccountBase.is_active == a)
if q is not None:
for name in q.split():
query_ = query_.where(AccountBase.name.ilike(f"%{name}%"))
query_ = query_.order_by(AccountBase.name)
if c is not None:
query_ = query_.limit(c)
data: list[AccountBase] = list(db.execute(query_).scalars().all())
for item in data:
list_.append(AccountLink(id_=item.id, name=item.name))
for item in data:
list_.append(AccountLink(id_=item.id, name=item.name))
return list_
@router.get("/{id_}/balance", response_model=AccountBalance)
async def show_balance(
id_: uuid.UUID,
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
d: str | None = None,
user: UserToken = Depends(get_user),
) -> AccountBalance:
date_ = None if d is None or d == "" else datetime.strptime(d, "%d-%b-%Y")
with SessionFuture() as db:
return AccountBalance(date_=balance(id_, date_, db), total=balance(id_, None, db))
return AccountBalance(date_=balance(id_, date_, db), total=balance(id_, None, db))
@router.get("/{id_}", response_model=schemas.Account)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["accounts"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["accounts"])], db: SessionDep
) -> schemas.Account:
with SessionFuture() as db:
item: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
return account_info(item)
item: Account = db.execute(select(Account).where(Account.id == id_)).scalar_one()
return account_info(item)
def balance(id_: uuid.UUID, date_: date | None, db: Session) -> Decimal:
+8 -7
View File
@@ -1,8 +1,10 @@
from fastapi import APIRouter, Depends
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import select
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_type import AccountType
from ..schemas import account_type as schemas
from ..schemas.user import UserToken
@@ -12,8 +14,7 @@ router = APIRouter()
@router.get("", response_model=list[schemas.AccountType])
def account_type_list(user: UserToken = Depends(get_user)) -> list[schemas.AccountType]:
with SessionFuture() as db:
return [
schemas.AccountType(id_=item.id, name=item.name) for item in db.execute(select(AccountType)).scalars().all()
]
def account_type_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.AccountType]:
return [
schemas.AccountType(id_=item.id, name=item.name) for item in db.execute(select(AccountType)).scalars().all()
]
+29 -37
View File
@@ -1,20 +1,20 @@
from collections.abc import Generator
from datetime import date, datetime, timedelta
from datetime import date, timedelta
from typing import Annotated
from fastapi import APIRouter, HTTPException, Request, Security, status
from fastapi import APIRouter, Depends, Security
from sqlalchemy import or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.attendance import Attendance
from ..models.employee import Employee
from ..routers.fingerprint import get_prints
from ..schemas import attendance as schemas
from ..schemas.attendance_type import AttendanceType as AttendanceTypeSchema
from ..schemas.user import UserToken
from .reports import path_date, report_date
router = APIRouter()
@@ -22,23 +22,22 @@ router = APIRouter()
@router.get("", response_model=schemas.Attendance)
def attendance_blank(
request: Request, user: UserToken = Security(get_user, scopes=["attendance"])
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
) -> schemas.Attendance:
return schemas.Attendance(date_=get_date(request.session), body=[]) # type: ignore[arg-type]
return schemas.Attendance(date_=date_, body=[])
@router.get("/{date_}", response_model=schemas.Attendance)
def attendance_date(
date_: str,
request: Request,
user: UserToken = Security(get_user, scopes=["attendance"]),
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
) -> schemas.Attendance:
set_date(date_, request.session)
with SessionFuture() as db:
return schemas.Attendance(
date_=date_, # type: ignore[arg-type]
body=attendance_date_report(datetime.strptime(date_, "%d-%b-%Y"), db),
)
return schemas.Attendance(
date_=date_,
body=attendance_date_report(date_, db),
)
def attendance_date_report(date_: date, db: Session) -> list[schemas.AttendanceItem]:
@@ -93,29 +92,22 @@ def attendance_date_report(date_: date, db: Session) -> list[schemas.AttendanceI
@router.post("/{date_}", response_model=schemas.Attendance)
def save(
date_: str,
date_: Annotated[date, Depends(path_date)],
data: schemas.Attendance,
user: UserToken = Security(get_user, scopes=["attendance"]),
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
) -> schemas.Attendance:
try:
att_date = datetime.strptime(date_, "%d-%b-%Y").date()
with SessionFuture() as db:
for item in data.body:
if item.attendance_type.id_ != 0:
attendance = Attendance(
employee_id=item.id_,
date_=att_date,
attendance_type=item.attendance_type.id_,
user_id=user.id_,
)
attendance.create(db)
db.commit()
return schemas.Attendance(date_=date_, body=attendance_date_report(att_date, db)) # type: ignore[arg-type]
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
for item in data.body:
if item.attendance_type.id_ != 0:
attendance = Attendance(
employee_id=item.id_,
date_=date_,
attendance_type=item.attendance_type.id_,
user_id=user.id_,
)
attendance.create(db)
db.commit()
return schemas.Attendance(date_=date_, body=attendance_date_report(date_, db))
def date_range(
+19 -17
View File
@@ -1,17 +1,19 @@
import csv
import io
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.attendance import Attendance
from ..models.attendance_type import AttendanceType
from ..models.employee import Employee
from ..routers.reports import report_finish_date, report_start_date
from .attendance import date_range
@@ -20,22 +22,22 @@ router = APIRouter()
@router.get("", response_class=StreamingResponse)
def get_report(
s: str,
f: str,
# user: UserToken = Security(get_user, scopes=["attendance"]) ## removed as jwt headers are a pain in the ass
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
# user: Annotated[UserToken, Security(get_user, scopes=["attendance"])] ## removed as jwt headers are a pain in the ass
db: SessionDep,
) -> StreamingResponse:
try:
with SessionFuture() as db:
output = io.StringIO()
attendance_record(
datetime.strptime(s, "%d-%b-%Y"),
datetime.strptime(f, "%d-%b-%Y"),
output,
db,
)
headers = {"Content-Disposition": "attachment; filename = attendance-record.csv"}
output.seek(0)
return StreamingResponse(output, media_type="text/csv", headers=headers)
output = io.StringIO()
attendance_record(
start_date,
finish_date,
output,
db,
)
headers = {"Content-Disposition": "attachment; filename = attendance-record.csv"}
output.seek(0)
return StreamingResponse(output, media_type="text/csv", headers=headers)
finally:
pass
# output.close()
+4 -2
View File
@@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends
from typing import Annotated
from fastapi import APIRouter, Security
from ..core.security import get_current_active_user as get_user
from ..models.attendance_type import AttendanceType
@@ -10,7 +12,7 @@ router = APIRouter()
@router.get("", response_model=list[schemas.AttendanceType])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.AttendanceType]:
async def show_list(user: Annotated[UserToken, Security(get_user)]) -> list[schemas.AttendanceType]:
list_ = AttendanceType.list()
attendance_types: list[schemas.AttendanceType] = []
for item in list_:
+45 -66
View File
@@ -1,11 +1,13 @@
import datetime
from fastapi import APIRouter, Depends
from sqlalchemy import and_, or_, select
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import select
from sqlalchemy.orm import contains_eager
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.batch import Batch
from ..models.product import Product
from ..models.product_version import ProductVersion
@@ -14,6 +16,7 @@ from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas import batch as schemas
from ..schemas.product import ProductLink
from ..schemas.user import UserToken
from . import _pv_onclause, _sv_onclause
router = APIRouter()
@@ -21,71 +24,47 @@ router = APIRouter()
@router.get("", response_model=list[schemas.Batch])
def batch_term(
q: str | None,
d: str,
current_user: UserToken = Depends(get_user),
q: str | None, d: str, user: Annotated[UserToken, Security(get_user)], db: SessionDep
) -> list[schemas.Batch]:
date_ = datetime.datetime.strptime(d, "%d-%b-%Y")
list_: list[schemas.Batch] = []
with SessionFuture() as db:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
query = (
select(Batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(SkuVersion, onclause=sku_version_onclause)
.where(Batch.quantity_remaining > 0, Batch.name <= date_)
.options(
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.versions),
)
)
if q is not None:
for q_item in q.split():
query = query.where(ProductVersion.name.ilike(f"%{q_item}%"))
result = db.execute(query.order_by(ProductVersion.name).order_by(Batch.name)).unique().scalars().all()
for item in result:
product_name = item.sku.product.versions[0].name
product_units = item.sku.versions[0].units
text = (
f"{product_name} ({product_units}) {item.quantity_remaining:.2f}@"
f"{item.rate:.2f} from {item.name.strftime('%d-%b-%Y')}"
query = (
select(Batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(SkuVersion, onclause=_sv_onclause(date_))
.where(Batch.quantity_remaining > 0, Batch.name <= date_)
.options(
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.versions),
)
)
if q is not None:
for q_item in q.split():
query = query.where(ProductVersion.name.ilike(f"%{q_item}%"))
result = db.execute(query.order_by(ProductVersion.name).order_by(Batch.name)).unique().scalars().all()
for item in result:
product_name = item.sku.product.versions[0].name
product_units = item.sku.versions[0].units
text = (
f"{product_name} ({product_units}) {item.quantity_remaining:.2f}@"
f"{item.rate:.2f} from {item.name.strftime('%d-%b-%Y')}"
)
list_.append(
schemas.Batch(
id_=item.id,
name=text,
quantity_remaining=round(item.quantity_remaining, 2),
rate=round(item.rate, 2),
tax=round(item.tax, 5),
discount=round(item.discount, 5),
sku=ProductLink(
id_=item.sku.id,
name=f"{product_name} ({product_units})",
),
)
list_.append(
schemas.Batch(
id_=item.id,
name=text,
quantity_remaining=round(item.quantity_remaining, 2),
rate=round(item.rate, 2),
tax=round(item.tax, 5),
discount=round(item.discount, 5),
sku=ProductLink(
id_=item.sku.id,
name=f"{product_name} ({product_units})",
),
)
)
return list_
)
return list_
+11 -33
View File
@@ -1,23 +1,24 @@
import uuid
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy import distinct, func, select, update
from sqlalchemy.orm import Session, contains_eager
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.batch import Batch
from ..models.cost_centre import CostCentre
from ..models.inventory import Inventory
from ..models.journal import Journal
from ..models.product import Product
from ..models.product_version import ProductVersion
from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.voucher import Voucher
from ..schemas import batch_integrity as schemas
from ..schemas.user import UserToken
from . import _pv_onclause
from .issue import refresh_voucher
@@ -26,28 +27,16 @@ router = APIRouter()
@router.post("", response_model=list[schemas.BatchIntegrity])
def post_check_batch_integrity(
user: UserToken = Security(get_user, scopes=["product-ledger"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])], db: SessionDep
) -> list[schemas.BatchIntegrity]:
with SessionFuture() as db:
info = negative_batches(db) + batch_dates(db)
fix_batch_prices(db)
db.commit()
return info
info = negative_batches(db) + batch_dates(db)
fix_batch_prices(db)
db.commit()
return info
def negative_batches(db: Session) -> list[schemas.BatchIntegrity]:
inv_sum = func.sum(Inventory.quantity * Journal.debit).label("quantity")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
list_: list[tuple[Batch, str, Decimal]] = db.execute(
select(Batch, ProductVersion.name, inv_sum)
.join(Batch.sku)
@@ -55,7 +44,7 @@ def negative_batches(db: Session) -> list[schemas.BatchIntegrity]:
.join(Batch.inventories)
.join(Inventory.voucher)
.join(Voucher.journals)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.where(
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
@@ -105,17 +94,6 @@ def batch_details(batch_id: uuid.UUID, db: Session) -> list[schemas.BatchIntegri
def batch_dates(db: Session) -> list[schemas.BatchIntegrity]:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
list_ = (
db.execute(
select(Batch)
@@ -123,7 +101,7 @@ def batch_dates(db: Session) -> list[schemas.BatchIntegrity]:
.join(StockKeepingUnit.product)
.join(Batch.inventories)
.join(Inventory.voucher)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.where(Voucher.date_ < Batch.name)
.options(
contains_eager(Batch.sku).contains_eager(StockKeepingUnit.product),
+32 -46
View File
@@ -2,9 +2,7 @@ import uuid
from datetime import date
from fastapi import HTTPException, status
from sqlalchemy import distinct, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..models.product import Product
@@ -19,28 +17,22 @@ from . import _pv_active, _pv_onclause, _sv_onclause
def calculate_nutrition(date_: date, db: Session) -> None:
try:
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(ProductVersion.product_group)
.where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
)
.scalars()
.all()
)
db.flush()
while len(products) > 0:
calculate(date_, products, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(ProductVersion.product_group)
.where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
)
.scalars()
.all()
)
db.flush()
while len(products) > 0:
calculate(date_, products, db)
def calculate(date_: date, products: set[uuid.UUID], db: Session) -> None:
@@ -118,26 +110,20 @@ def calculate(date_: date, products: set[uuid.UUID], db: Session) -> None:
def report_nutrition(date_: date, db: Session) -> list[NutritionalInformation]:
try:
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(ProductVersion.product_group)
)
.scalars()
.all()
)
final, semi, ingredients = nut_final(db), nut_semi(db), nut_ingredients(db)
return report(date_, products, final, semi, ingredients, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(ProductVersion.product_group)
)
.scalars()
.all()
)
final, semi, ingredients = nut_final(db), nut_semi(db), nut_ingredients(db)
return report(date_, products, final, semi, ingredients, db)
def report(
@@ -160,8 +146,8 @@ def report(
if ingredient.id in products:
raise ValueError("Ingredient cannot be in a recipe")
ingredients.remove(ingredient.id)
ingredient_dict[ingredient.name] = set([ingredient.name])
allergen_dict[ingredient.name] = set(a.strip() for a in ingredient.allergen.split(",")) - set([""])
ingredient_dict[ingredient.name] = {ingredient.name}
allergen_dict[ingredient.name] = {a.strip() for a in ingredient.allergen.split(",")} - {""}
_list.append(
NutritionalInformation(
name=ingredient.name,
@@ -206,7 +192,7 @@ def report(
)
allergen_dict[semi_product.name] = set.union(
*[allergen_dict[i.product.versions[0].name] for i in semi_recipe.items]
) - set([""])
) - {""}
_list.append(
NutritionalInformation(
@@ -241,7 +227,7 @@ def report(
)
allergen_dict[recipe_product.name] = set.union(
*[allergen_dict[i.product.versions[0].name] for i in recipe.items]
) - set([""])
) - {""}
_list.append(
NutritionalInformation(
name=recipe_product.name,
+26 -63
View File
@@ -2,10 +2,8 @@ import uuid
from decimal import Decimal
from fastapi import HTTPException, status
from sqlalchemy import and_, distinct, func, or_, select
from sqlalchemy import distinct, func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..models.batch import Batch
@@ -20,34 +18,29 @@ from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from . import _sv_onclause
def calculate_prices(period_id: uuid.UUID, db: Session) -> None:
try:
item: Period = db.execute(select(Period).where(Period.id == period_id)).scalar_one()
recipes = set(
db.execute(select(distinct(StockKeepingUnit.product_id)).join(StockKeepingUnit.recipes)).scalars().all()
)
ingredients = set(db.execute(select(distinct(RecipeItem.product_id))).scalars().all())
issued_products = get_issue_prices(item, ingredients - recipes, db)
left = ingredients - recipes - issued_products.keys()
purchased_products = get_purchase_prices(item, left, db)
left -= purchased_products.keys()
rest = get_rest(left, db)
prices = issued_products | purchased_products | rest
item: Period = db.execute(select(Period).where(Period.id == period_id)).scalar_one()
recipes = set(
db.execute(select(distinct(StockKeepingUnit.product_id)).join(StockKeepingUnit.recipes)).scalars().all()
)
ingredients = set(db.execute(select(distinct(RecipeItem.product_id))).scalars().all())
issued_products = get_issue_prices(item, ingredients - recipes, db)
left = ingredients - recipes - issued_products.keys()
purchased_products = get_purchase_prices(item, left, db)
left -= purchased_products.keys()
rest = get_rest(left, db)
prices = issued_products | purchased_products | rest
while len(recipes) > 0:
calculate_recipes(recipes, prices, db)
for pid, price in prices.items():
db.execute(
pg_insert(Price)
.values(product_id=pid, price=price, period_id=item.id)
.on_conflict_do_update(constraint="uq_prices_period_id", set_=dict(price=price))
)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
while len(recipes) > 0:
calculate_recipes(recipes, prices, db)
for pid, price in prices.items():
db.execute(
pg_insert(Price)
.values(product_id=pid, price=price, period_id=item.id)
.on_conflict_do_update(constraint="uq_prices_period_id", set_={"price": price})
)
@@ -57,24 +50,14 @@ def get_issue_prices(period: Period, products: set[uuid.UUID], db: Session) -> d
)
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
issue_price = (sum_net / sum_quantity).label("issue_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
d: dict[uuid.UUID, Decimal] = {}
query = db.execute(
select(StockKeepingUnit.product_id, issue_price)
.select_from(Inventory)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(Inventory.voucher)
.join(Voucher.journals)
.where(
@@ -97,17 +80,7 @@ def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dic
)
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
purchase_price = (sum_net / sum_quantity).label("purchase_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
d: dict[uuid.UUID, Decimal] = {}
query = db.execute(
@@ -115,7 +88,7 @@ def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dic
.select_from(Inventory)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(Inventory.voucher)
.join(Voucher.journals)
.where(
@@ -133,24 +106,14 @@ def get_purchase_prices(period: Period, req: set[uuid.UUID], db: Session) -> dic
def get_rest(req: set[uuid.UUID], db: Session) -> dict[uuid.UUID, Decimal]:
rest_price = (SkuVersion.cost_price / (SkuVersion.fraction * SkuVersion.product_yield)).label("rest_price")
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
d: dict[uuid.UUID, Decimal] = {}
query = db.execute(
select(
StockKeepingUnit.product_id,
rest_price,
)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.where(StockKeepingUnit.product_id.in_(req))
).all()
for id, amount in query:
+44 -59
View File
@@ -1,11 +1,12 @@
import uuid
from fastapi import APIRouter, HTTPException, Security, status
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import delete, select
from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.client import Client
from ..models.login_history import LoginHistory
from ..schemas import client as schemas
@@ -19,76 +20,60 @@ router = APIRouter()
def update_route(
id_: uuid.UUID,
data: schemas.ClientIn,
user: UserToken = Security(get_user, scopes=["clients"]),
user: Annotated[UserToken, Security(get_user, scopes=["clients"])],
db: SessionDep,
) -> None:
try:
with SessionFuture() as db:
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
item.enabled = data.enabled
if item.enabled:
item.otp = None
item.name = data.name
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
item.enabled = data.enabled
if item.enabled:
item.otp = None
item.name = data.name
db.commit()
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["clients"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["clients"])], db: SessionDep
) -> None:
try:
with SessionFuture() as db:
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
db.execute(delete(LoginHistory).where(LoginHistory.client_id == item.id))
db.delete(item)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
db.execute(delete(LoginHistory).where(LoginHistory.client_id == item.id))
db.delete(item)
db.commit()
@router.get("/list", response_model=list[schemas.ClientList])
async def show_list(
user: UserToken = Security(get_user, scopes=["clients"]),
user: Annotated[UserToken, Security(get_user, scopes=["clients"])],
db: SessionDep,
) -> list[schemas.ClientList]:
with SessionFuture() as db:
list_ = db.execute(select(Client).order_by(Client.name)).scalars().all()
clients = []
for item in list_:
clients.append(
schemas.ClientList(
id_=item.id,
code=item.code,
name=item.name,
enabled=item.enabled,
otp=item.otp,
creation_date=item.creation_date,
last_user=item.login_history[0].user.name if len(item.login_history) else "None",
last_date=item.login_history[0].date if len(item.login_history) else None,
)
list_ = db.execute(select(Client).order_by(Client.name)).scalars().all()
clients = []
for item in list_:
clients.append(
schemas.ClientList(
id_=item.id,
code=item.code,
name=item.name,
enabled=item.enabled,
otp=item.otp,
creation_date=item.creation_date,
last_user=item.login_history[0].user.name if len(item.login_history) else "None",
last_date=item.login_history[0].date if len(item.login_history) else None,
)
return clients
)
return clients
@router.get("/{id_}", response_model=schemas.Client)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["clients"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["clients"])], db: SessionDep
) -> schemas.Client:
with SessionFuture() as db:
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
return schemas.Client(
id_=item.id,
code=item.code,
name=item.name,
enabled=item.enabled,
otp=item.otp,
creation_date=item.creation_date,
)
item: Client = db.execute(select(Client).where(Client.id == id_)).scalar_one()
return schemas.Client(
id_=item.id,
code=item.code,
name=item.name,
enabled=item.enabled,
otp=item.otp,
creation_date=item.creation_date,
)
+40 -62
View File
@@ -1,11 +1,12 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, Security, status
from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import delete, select, update
from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.cost_centre import CostCentre
from ..models.journal import Journal
@@ -18,95 +19,72 @@ router = APIRouter()
@router.post("", response_model=schemas.CostCentre)
def save(
data: schemas.CostCentreIn,
user: UserToken = Security(get_user, scopes=["cost-centres"]),
data: schemas.CostCentreIn, user: Annotated[UserToken, Security(get_user, scopes=["cost-centres"])], db: SessionDep
) -> schemas.CostCentre:
try:
with SessionFuture() as db:
item = CostCentre(name=data.name)
db.add(item)
db.commit()
return cost_centre_info(item)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = CostCentre(name=data.name)
db.add(item)
db.commit()
return cost_centre_info(item)
@router.put("/{id_}", response_model=schemas.CostCentre)
def update_route(
id_: uuid.UUID,
data: schemas.CostCentreIn,
user: UserToken = Security(get_user, scopes=["cost-centres"]),
user: Annotated[UserToken, Security(get_user, scopes=["cost-centres"])],
db: SessionDep,
) -> schemas.CostCentre:
try:
with SessionFuture() as db:
item = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
db.commit()
return cost_centre_info(item)
except SQLAlchemyError as e:
item = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
db.commit()
return cost_centre_info(item)
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["cost-centres"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["cost-centres"])], db: SessionDep
) -> None:
with SessionFuture() as db:
item: CostCentre = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
db.execute(
update(AccountBase)
.where(AccountBase.cost_centre_id == item.id)
.values(cost_centre_id=CostCentre.cost_centre_overall())
item: CostCentre = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
db.execute(
update(Journal)
.where(Journal.cost_centre_id == item.id)
.values(cost_centre_id=CostCentre.cost_centre_overall())
)
db.execute(delete(CostCentre).where(CostCentre.id == item.id))
db.commit()
db.execute(
update(AccountBase)
.where(AccountBase.cost_centre_id == item.id)
.values(cost_centre_id=CostCentre.cost_centre_overall())
)
db.execute(
update(Journal).where(Journal.cost_centre_id == item.id).values(cost_centre_id=CostCentre.cost_centre_overall())
)
db.execute(delete(CostCentre).where(CostCentre.id == item.id))
db.commit()
@router.get("", response_model=schemas.CostCentreBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["cost-centres"]),
user: Annotated[UserToken, Security(get_user, scopes=["cost-centres"])],
) -> schemas.CostCentreBlank:
return cost_centre_blank()
@router.get("/list", response_model=list[schemas.CostCentre])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.CostCentre]:
with SessionFuture() as db:
return [
cost_centre_info(item) for item in db.execute(select(CostCentre).order_by(CostCentre.name)).scalars().all()
]
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.CostCentre]:
return [cost_centre_info(item) for item in db.execute(select(CostCentre).order_by(CostCentre.name)).scalars().all()]
@router.get("/{id_}", response_model=schemas.CostCentre)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["cost-centres"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["cost-centres"])], db: SessionDep
) -> schemas.CostCentre:
with SessionFuture() as db:
item = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
return cost_centre_info(item)
item = db.execute(select(CostCentre).where(CostCentre.id == id_)).scalar_one()
return cost_centre_info(item)
def cost_centre_info(item: CostCentre) -> schemas.CostCentre:
+19 -18
View File
@@ -1,14 +1,14 @@
from calendar import monthrange
from datetime import date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Body, HTTPException, Security, status
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_first_day, get_last_day
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.attendance import Attendance
from ..models.attendance_type import AttendanceType
@@ -18,6 +18,7 @@ from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..schemas.message import Message
from ..schemas.user import UserToken
from .reports import get_first_day, get_last_day
router = APIRouter()
@@ -25,27 +26,27 @@ router = APIRouter()
@router.post("", response_model=Message)
def credit_salary(
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
month: str = Body(..., embed=True),
user: UserToken = Security(get_user, scopes=["attendance"]),
) -> Message:
month_ = datetime.strptime(month, "%d-%b-%Y").date()
start_date = get_first_day(month_)
finish_date = get_last_day(month_)
with SessionFuture() as db:
voucher = Voucher(
date_=finish_date,
narration="Auto Generated Salary Entry",
user_id=user.id_,
voucher_type=VoucherType.JOURNAL,
posted=True,
poster_id=user.id_,
)
db.add(voucher)
for item in salary_journals(start_date, finish_date, db):
voucher.journals.append(item)
db.add(item)
db.commit()
return Message(message="Salary Entry created")
voucher = Voucher(
date_=finish_date,
narration="Auto Generated Salary Entry",
user_id=user.id_,
voucher_type=VoucherType.JOURNAL,
posted=True,
poster_id=user.id_,
)
db.add(voucher)
for item in salary_journals(start_date, finish_date, db):
voucher.journals.append(item)
db.add(item)
db.commit()
return Message(message="Salary Entry created")
def salary_journals(start_date: date, finish_date: date, db: Session) -> list[Journal]:
+7 -8
View File
@@ -7,7 +7,7 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.db_image import DbImage
from ..schemas import image_upload as output
@@ -16,17 +16,16 @@ router = APIRouter()
@router.get("/{id_}/{type_}", response_class=StreamingResponse)
def db_image(id_: uuid.UUID, type_: str) -> StreamingResponse:
with SessionFuture() as db:
item = db.execute(select(DbImage).where(DbImage.id == id_)).scalar_one()
img = BytesIO(item.thumbnail) if type_ == "thumbnail" else BytesIO(item.image)
return StreamingResponse(img, media_type="image/jpeg")
def db_image(id_: uuid.UUID, type_: str, db: SessionDep) -> StreamingResponse:
item = db.execute(select(DbImage).where(DbImage.id == id_)).scalar_one()
img = BytesIO(item.thumbnail) if type_ == "thumbnail" else BytesIO(item.image)
return StreamingResponse(img, media_type="image/jpeg")
def save_files(voucher_id: uuid.UUID, i: list[bytes], t: list[bytes], db: Session) -> None:
i = i or []
t = t or []
for index, value in enumerate(i):
for index, _value in enumerate(i):
db.add(DbImage(voucher_id, "voucher", i[index], t[index]))
@@ -43,5 +42,5 @@ def update_files(
images = db.execute(select(DbImage).where(DbImage.resource_id == voucher_id)).scalars().all()
for image in [i for i in images if i.id not in old]:
db.delete(image)
for index, value in enumerate(i):
for index, _value in enumerate(i):
db.add(DbImage(voucher_id, "voucher", i[index], t[index]))
+9 -8
View File
@@ -1,9 +1,11 @@
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import delete, desc, distinct, func, over, select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.attendance import Attendance
from ..schemas.settings import AttendanceCount
from ..schemas.user import UserToken
@@ -13,13 +15,12 @@ router = APIRouter()
@router.post("", response_model=AttendanceCount)
def post_check_db(user: UserToken = Security(get_user)) -> AttendanceCount:
with SessionFuture() as db:
duplicate_attendances = get_duplicate_attendances(db)
if duplicate_attendances > 0:
fix_duplicate_attendances(db)
db.commit()
return AttendanceCount(attendance_count=duplicate_attendances)
def post_check_db(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> AttendanceCount:
duplicate_attendances = get_duplicate_attendances(db)
if duplicate_attendances > 0:
fix_duplicate_attendances(db)
db.commit()
return AttendanceCount(attendance_count=duplicate_attendances)
def get_duplicate_attendances(db: Session) -> int:
+86 -105
View File
@@ -3,14 +3,14 @@ import uuid
from collections.abc import Sequence
from datetime import datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import desc, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, joinedload
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.cost_centre import CostCentre
from ..models.employee import Employee
@@ -27,148 +27,129 @@ router = APIRouter()
@router.post("", response_model=schemas.Employee)
def save(
data: schemas.EmployeeIn,
user: UserToken = Security(get_user, scopes=["employees"]),
data: schemas.EmployeeIn, user: Annotated[UserToken, Security(get_user, scopes=["employees"])], db: SessionDep
) -> schemas.Employee:
try:
with SessionFuture() as db:
item = Employee(
name=data.name,
code=None,
is_starred=data.is_starred,
is_active=data.is_active,
cost_centre_id=data.cost_centre.id_,
designation=data.designation,
salary=data.salary,
points=data.points,
joining_date=data.joining_date,
leaving_date=None if data.is_active else data.leaving_date,
).create(db)
db.commit()
return employee_info(item)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = Employee(
name=data.name,
code=None,
is_starred=data.is_starred,
is_active=data.is_active,
cost_centre_id=data.cost_centre.id_,
designation=data.designation,
salary=data.salary,
points=data.points,
joining_date=data.joining_date,
leaving_date=None if data.is_active else data.leaving_date,
).create(db)
db.commit()
return employee_info(item)
@router.put("/{id_}", response_model=schemas.Employee)
def update_route(
id_: uuid.UUID,
data: schemas.EmployeeIn,
user: UserToken = Security(get_user, scopes=["employees"]),
user: Annotated[UserToken, Security(get_user, scopes=["employees"])],
db: SessionDep,
) -> schemas.Employee:
try:
with SessionFuture() as db:
item: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.cost_centre_id = data.cost_centre.id_
item.designation = data.designation
item.salary = data.salary
item.points = data.points
item.joining_date = data.joining_date
item.is_starred = data.is_starred
item.is_active = data.is_active
item.leaving_date = data.leaving_date
db.commit()
return employee_info(item)
except SQLAlchemyError as e:
item: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.cost_centre_id = data.cost_centre.id_
item.designation = data.designation
item.salary = data.salary
item.points = data.points
item.joining_date = data.joining_date
item.is_starred = data.is_starred
item.is_active = data.is_active
item.leaving_date = data.leaving_date
db.commit()
return employee_info(item)
@router.delete("/{id_}", response_model=schemas.EmployeeBlank)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["employees"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["employees"])], db: SessionDep
) -> schemas.EmployeeBlank:
with SessionFuture() as db:
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
can_delete, reason = employee.can_delete("advanced-delete" in user.permissions)
if can_delete:
delete_with_data(employee, db)
db.commit()
return employee_blank()
else:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Cannot delete account because {reason}",
)
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
can_delete, reason = employee.can_delete("advanced-delete" in user.permissions)
if can_delete:
delete_with_data(employee, db)
db.commit()
return employee_blank()
else:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Cannot delete account because {reason}",
)
@router.get("", response_model=schemas.EmployeeBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["employees"]),
user: Annotated[UserToken, Security(get_user, scopes=["employees"])],
) -> schemas.EmployeeBlank:
return employee_blank()
@router.get("/list", response_model=list[schemas.Employee])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Employee]:
with SessionFuture() as db:
return [
employee_info(item)
for item in db.execute(
select(Employee)
.order_by(desc(Employee.is_active))
.order_by(Account.cost_centre_id)
.order_by(Employee.designation)
.order_by(Employee.name)
)
.scalars()
.all()
]
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.Employee]:
return [
employee_info(item)
for item in db.execute(
select(Employee)
.order_by(desc(Employee.is_active))
.order_by(Account.cost_centre_id)
.order_by(Employee.designation)
.order_by(Employee.name)
)
.scalars()
.all()
]
@router.get("/query", response_model=list[schemas.EmployeeLink])
async def show_term(
q: str,
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
c: int | None = None,
current_user: UserToken = Depends(get_user),
) -> list[schemas.EmployeeLink]:
list_: list[schemas.EmployeeLink] = []
with SessionFuture() as db:
query_ = select(Employee)
if q is not None:
for name in q.split():
query_ = query_.where(Employee.name.ilike(f"%{name}%"))
query_ = query_.order_by(Employee.name)
if c is not None:
query_ = query_.limit(c)
data: Sequence[Employee] = db.execute(query_).scalars().all()
query_ = select(Employee)
if q is not None:
for name in q.split():
query_ = query_.where(Employee.name.ilike(f"%{name}%"))
query_ = query_.order_by(Employee.name)
if c is not None:
query_ = query_.limit(c)
data: Sequence[Employee] = db.execute(query_).scalars().all()
for item in data:
list_.append(
schemas.EmployeeLink(
id_=item.id,
name=item.name,
designation=item.designation,
cost_centre=CostCentreLink(
id_=item.cost_centre.id,
name=item.cost_centre.name,
),
)
for item in data:
list_.append(
schemas.EmployeeLink(
id_=item.id,
name=item.name,
designation=item.designation,
cost_centre=CostCentreLink(
id_=item.cost_centre.id,
name=item.cost_centre.name,
),
)
)
return list_
@router.get("/{id_}", response_model=schemas.Employee)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["employees"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["employees"])], db: SessionDep
) -> schemas.Employee:
with SessionFuture() as db:
item: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
return employee_info(item)
item: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
return employee_info(item)
def employee_info(employee: Employee) -> schemas.Employee:
+50 -51
View File
@@ -1,17 +1,18 @@
import uuid
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_finish_date, get_start_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.attendance import Attendance
from ..models.employee import Employee
from ..routers.fingerprint import get_prints
from ..routers.reports import report_finish_date, report_start_date
from ..schemas import employee_attendance as schemas
from ..schemas.account import AccountLink
from ..schemas.attendance_type import AttendanceType as AttendanceTypeSchema
@@ -24,12 +25,13 @@ router = APIRouter()
@router.get("", response_model=schemas.EmployeeAttendance)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["attendance"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
) -> schemas.EmployeeAttendance:
return schemas.EmployeeAttendance(
start_date=get_start_date(request.session), # type: ignore[arg-type]
finish_date=get_finish_date(request.session), # type: ignore[arg-type]
start_date=start_date,
finish_date=finish_date,
employee=None,
body=[],
)
@@ -38,28 +40,25 @@ def show_blank(
@router.get("/{id_}", response_model=schemas.EmployeeAttendance)
def employee_attendance_report(
id_: uuid.UUID,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["attendance"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
) -> schemas.EmployeeAttendance:
with SessionFuture() as db:
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
start_date = datetime.strptime(s or get_start_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
finish_date = (
employee.leaving_date
if not employee.is_active and employee.leaving_date and employee.leaving_date < finish_date
else finish_date
)
info = schemas.EmployeeAttendance(
start_date=start_date,
finish_date=finish_date,
employee=AccountLink(id_=employee.id, name=employee.name),
body=employee_attendance(employee, start_date, finish_date, db),
)
return info
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
start_date = employee.joining_date if employee.joining_date > start_date else start_date
finish_date = (
employee.leaving_date
if not employee.is_active and employee.leaving_date and employee.leaving_date < finish_date
else finish_date
)
info = schemas.EmployeeAttendance(
start_date=start_date,
finish_date=finish_date,
employee=AccountLink(id_=employee.id, name=employee.name),
body=employee_attendance(employee, start_date, finish_date, db),
)
return info
def employee_attendance(
@@ -97,26 +96,26 @@ def employee_attendance(
def save_employee_attendance(
id_: uuid.UUID,
data: schemas.EmployeeAttendance,
user: UserToken = Security(get_user, scopes=["attendance"]),
user: Annotated[UserToken, Security(get_user, scopes=["attendance"])],
db: SessionDep,
) -> schemas.EmployeeAttendance:
with SessionFuture() as db:
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
for item in data.body:
attendance_type = item.attendance_type.id_
if attendance_type != 0:
attendance = Attendance(
employee_id=employee.id,
date_=item.date_,
attendance_type=attendance_type,
user_id=user.id_,
)
attendance.create(db)
db.commit()
start_date = min(i.date_ for i in data.body)
finish_date = max(i.date_ for i in data.body)
return schemas.EmployeeAttendance(
start_date=start_date,
finish_date=finish_date,
employee=AccountLink(id_=employee.id, name=employee.name),
body=employee_attendance(employee, start_date, finish_date, db),
)
employee: Employee = db.execute(select(Employee).where(Employee.id == id_)).scalar_one()
for item in data.body:
attendance_type = item.attendance_type.id_
if attendance_type != 0:
attendance = Attendance(
employee_id=employee.id,
date_=item.date_,
attendance_type=attendance_type,
user_id=user.id_,
)
attendance.create(db)
db.commit()
start_date = min(i.date_ for i in data.body)
finish_date = max(i.date_ for i in data.body)
return schemas.EmployeeAttendance(
start_date=start_date,
finish_date=finish_date,
employee=AccountLink(id_=employee.id, name=employee.name),
body=employee_attendance(employee, start_date, finish_date, db),
)
+39 -59
View File
@@ -3,15 +3,14 @@ import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from math import ceil
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import distinct, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, get_last_day, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.employee import Employee
from ..models.employee_benefit import EmployeeBenefit
@@ -26,6 +25,7 @@ from ..schemas.employee_benefit import EmployeeBenefit as EmployeeBenefitSchema
from ..schemas.user import UserToken
from . import get_lock_info
from .db_image import save_files, update_files
from .reports import get_last_day, report_date
from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info
@@ -35,29 +35,23 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["employee-benefit"])],
db: SessionDep,
data: schema_in.EmployeeBenefitIn = Depends(schema_in.EmployeeBenefitIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["employee-benefit"]),
) -> output.Voucher:
try:
dt = get_last_day(data.date_)
days_in_month = dt.day
with SessionFuture() as db:
item: Voucher = save(data, dt, user, db)
exp, total = save_employee_benefits(item, data.employee_benefits, days_in_month, db)
save_journals(item, exp, total, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
dt = get_last_day(data.date_)
days_in_month = dt.day
item: Voucher = save(data, dt, user, db)
exp, total = save_employee_benefits(item, data.employee_benefits, days_in_month, db)
save_journals(item, exp, total, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.EmployeeBenefitIn, date_: date, user: UserToken, db: Session) -> Voucher:
@@ -149,33 +143,27 @@ def save_journals(voucher: Voucher, exp: int, total: int, db: Session) -> None:
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["employee-benefit"])],
db: SessionDep,
data: schema_in.EmployeeBenefitIn = Depends(schema_in.EmployeeBenefitIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["employee-benefit"]),
) -> output.Voucher:
try:
dt = get_last_day(data.date_)
days_in_month = dt.day
with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, db)
if dt != item.date_:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail="Date Cannot be changed for Employee Benefit voucher!",
)
exp, total = update_employee_benefits(item, data.employee_benefits, days_in_month, db)
update_journals(item, Decimal(exp), Decimal(total))
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return voucher_info(item, db)
except SQLAlchemyError as e:
dt = get_last_day(data.date_)
days_in_month = dt.day
item: Voucher = update_voucher(id_, data, user, db)
if dt != item.date_:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail="Date Cannot be changed for Employee Benefit voucher!",
)
exp, total = update_employee_benefits(item, data.employee_benefits, days_in_month, db)
update_journals(item, Decimal(exp), Decimal(total))
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
db.commit()
report_date(request, data.date_)
return voucher_info(item, db)
def update_voucher(id_: uuid.UUID, data: schema_in.EmployeeBenefitIn, user: UserToken, db: Session) -> Voucher:
@@ -236,28 +224,20 @@ def update_journals(voucher: Voucher, exp: Decimal, total: Decimal) -> None:
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["employee-benefit"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["employee-benefit"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["employee-benefit"]),
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["employee-benefit"])],
db: SessionDep,
) -> output.Voucher:
additional_info = BlankVoucherInfo(date_=get_date(request.session), type_=VoucherType.EMPLOYEE_BENEFIT) # type: ignore[arg-type]
with SessionFuture() as db:
return blank_voucher(additional_info, db)
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.EMPLOYEE_BENEFIT)
return blank_voucher(additional_info, db)
def esi_contribution(gross_salary: int, days_worked: int, days_in_month: int) -> tuple[int, int, int]:
+24 -25
View File
@@ -1,53 +1,52 @@
import csv
import io
import logging
import uuid
from datetime import date, datetime, time, timedelta
from io import StringIO
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi import APIRouter, File, Security, UploadFile
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.employee import Employee
from ..models.fingerprint import Fingerprint
from ..schemas import fingerprint as schemas
from ..schemas.user import UserToken
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("", response_model=None)
def upload_prints(
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
fingerprints: UploadFile = File(None),
user: UserToken = Depends(get_user),
) -> None:
try:
with SessionFuture() as db:
start = date.today() - timedelta(days=90)
finish = date.today() + timedelta(days=7)
employees: dict[int, uuid.UUID] = {}
for id_, code in db.execute(select(Employee.id, Employee.code)).all():
employees[code] = id_
file_data = read_file(fingerprints)
prints = [d.model_dump() for d in fp(file_data, employees) if start <= d.date_.date() <= finish]
for p in prints:
p["id"] = p.pop("id_")
paged_data = [prints[i : i + 100] for i in range(0, len(prints), 100)]
for i, page in enumerate(paged_data):
print(f"Processing page {i} of {len(paged_data)}")
db.execute(pg_insert(Fingerprint).on_conflict_do_nothing(), page)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
start = date.today() - timedelta(days=90)
finish = date.today() + timedelta(days=7)
employees: dict[int, uuid.UUID] = {}
for id_, code in db.execute(select(Employee.id, Employee.code)).all():
employees[code] = id_
file_data = read_file(fingerprints)
prints = [d.model_dump() for d in fp(file_data, employees) if start <= d.date_.date() <= finish]
for p in prints:
logger.info(f"Processing fingerprint: {p}")
p["id"] = p.pop("id_")
paged_data = [prints[i : i + 100] for i in range(0, len(prints), 100)]
for i, page in enumerate(paged_data):
logger.info(f"Processing page {i} of {len(paged_data)}")
db.execute(pg_insert(Fingerprint).on_conflict_do_nothing(), page)
db.commit()
def read_file(input_file: UploadFile) -> io.StringIO:
+13 -13
View File
@@ -8,7 +8,7 @@ from fastapi.responses import StreamingResponse
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.employee import Employee
from .attendance import date_range
from .fingerprint import get_prints
@@ -21,20 +21,20 @@ router = APIRouter()
def get_report(
s: str,
f: str,
# user: UserToken = Security(get_user, scopes=["attendance"]) ## removed as jwt headers are a pain in the ass
# user: Annotated[UserToken, Security(get_user, scopes=["attendance"])] ## removed as jwt headers are a pain in the ass
db: SessionDep,
) -> StreamingResponse:
try:
with SessionFuture() as db:
output = io.StringIO()
attendance_record(
datetime.strptime(s, "%d-%b-%Y"),
datetime.strptime(f, "%d-%b-%Y"),
output,
db,
)
headers = {"Content-Disposition": "attachment; filename = fingerprint-record.csv"}
output.seek(0)
return StreamingResponse(output, media_type="text/csv", headers=headers)
output = io.StringIO()
attendance_record(
datetime.strptime(s, "%d-%b-%Y"),
datetime.strptime(f, "%d-%b-%Y"),
output,
db,
)
headers = {"Content-Disposition": "attachment; filename = fingerprint-record.csv"}
output.seek(0)
return StreamingResponse(output, media_type="text/csv", headers=headers)
finally:
pass
# output.close()
+17
View File
@@ -0,0 +1,17 @@
from fastapi import APIRouter, HTTPException
from sqlalchemy import text
from ..db.session import SessionDep
from ..schemas.health_response import HealthResponse
router = APIRouter()
@router.get("", response_model=HealthResponse)
def health_check(db: SessionDep) -> HealthResponse:
try:
db.execute(text("SELECT 1")) # lightweight DB check
return HealthResponse(status="ok", database="connected")
except Exception as e:
raise HTTPException(status_code=500, detail={"status": "error", "database": str(e)}) from e
+40 -60
View File
@@ -2,15 +2,14 @@ import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from sqlalchemy import and_, distinct, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, get_first_day, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.account_base import AccountBase
from ..models.attendance import Attendance
@@ -27,6 +26,7 @@ from ..schemas.blank_voucher_info import BlankVoucherInfo
from ..schemas.incentive import Incentive as IncentiveSchema
from ..schemas.user import UserToken
from . import get_lock_info
from .reports import get_first_day, report_date
from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info
@@ -36,27 +36,21 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["incentive"])],
db: SessionDep,
data: schema_in.IncentiveIn = Depends(schema_in.IncentiveIn.load_form),
user: UserToken = Security(get_user, scopes=["incentive"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = save(data, user, db)
employees = get_employees(get_first_day(data.date_), data.date_, data.incentives, None, db)
amount = balance(data.date_, None, db) * Decimal(0.9) # 10% for Deb Dip
total_points = sum(e.points * e.days_worked for e in employees)
point_value = round(amount / total_points, 2)
save_incentives(item, employees, point_value, db)
check_journals_are_valid(item)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = save(data, user, db)
employees = get_employees(get_first_day(data.date_), data.date_, data.incentives, None, db)
amount = balance(data.date_, None, db) * Decimal(0.9) # 10% for Deb Dip
total_points = sum(e.points * e.days_worked for e in employees)
point_value = round(amount / total_points, 2)
save_incentives(item, employees, point_value, db)
check_journals_are_valid(item)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.IncentiveIn, user: UserToken, db: Session) -> Voucher:
@@ -116,26 +110,20 @@ def save_incentives(
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["incentive"])],
db: SessionDep,
data: schema_in.IncentiveIn = Depends(schema_in.IncentiveIn.load_form),
user: UserToken = Security(get_user, scopes=["incentive"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, db)
employees = get_employees(get_first_day(data.date_), data.date_, data.incentives, item.journals, db)
amount = balance(data.date_, item.id, db) * Decimal(0.9) # 10% for Deb Dip
total_points = sum(e.points * e.days_worked for e in employees)
point_value = round(amount / total_points, 2)
update_incentives(item, employees, point_value, db)
check_journals_are_valid(item)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = update_voucher(id_, data, user, db)
employees = get_employees(get_first_day(data.date_), data.date_, data.incentives, item.journals, db)
amount = balance(data.date_, item.id, db) * Decimal(0.9) # 10% for Deb Dip
total_points = sum(e.points * e.days_worked for e in employees)
point_value = round(amount / total_points, 2)
update_incentives(item, employees, point_value)
check_journals_are_valid(item)
db.commit()
report_date(request, data.date_)
return voucher_info(item, db)
def update_voucher(id_: uuid.UUID, data: schema_in.IncentiveIn, user: UserToken, db: Session) -> Voucher:
@@ -170,7 +158,6 @@ def update_incentives(
voucher: Voucher,
employees: list[schema_in.IncentiveEmployee],
point_value: Decimal,
db: Session,
) -> None:
total_amount = 0
for item in voucher.incentives:
@@ -188,28 +175,21 @@ def update_incentives(
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["incentive"]),
user: Annotated[UserToken, Security(get_user, scopes=["incentive"])],
db: SessionDep,
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
d: str | None = None,
user: UserToken = Security(get_user, scopes=["incentive"]),
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["incentive"])],
db: SessionDep,
) -> output.Voucher:
additional_info = BlankVoucherInfo(date_=d or get_date(request.session), type_=VoucherType.INCENTIVE) # type: ignore[arg-type]
with SessionFuture() as db:
return blank_voucher(additional_info, db)
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.INCENTIVE)
return blank_voucher(additional_info, db)
def get_employees(
@@ -285,10 +265,10 @@ def check_if_employees_changed(
db: list[Employee],
voucher: list[Journal] | None,
) -> None:
json_ids = set(x.employee_id for x in json)
db_ids = set(x.id for x in db)
json_ids = {x.employee_id for x in json}
db_ids = {x.id for x in db}
voucher_ids = (
set(x.account_id for x in voucher if x.account_id != Account.incentive_id()) if voucher is not None else None
{x.account_id for x in voucher if x.account_id != Account.incentive_id()} if voucher is not None else None
)
if voucher_ids is None:
if len(json_ids ^ db_ids) != 0:
+53 -148
View File
@@ -1,16 +1,15 @@
import uuid
from datetime import UTC, datetime
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.batch import Batch
from ..models.cost_centre import CostCentre
@@ -29,8 +28,9 @@ from ..schemas.blank_voucher_info import BlankVoucherInfo
from ..schemas.cost_centre import CostCentreLink
from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken
from . import get_lock_info
from . import _pv_active, _pv_onclause, _sv_active, get_lock_info
from .db_image import save_files, update_files
from .reports import report_date
from .voucher import (
blank_voucher,
check_voucher_edit_allowed,
@@ -45,45 +45,29 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["issue"])],
db: SessionDep,
data: schema_in.IssueIn = Depends(schema_in.IssueIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["issue"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item, batch_consumed = save(data, user, db)
amount = save_inventories(item, data.inventories, batch_consumed, db)
check_duplicate_batches(item)
save_journals(item, data.source, data.destination, amount, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item, batch_consumed = save(data, user, db)
amount = save_inventories(item, data.inventories, batch_consumed, db)
check_duplicate_batches(item)
save_journals(item, data.source, data.destination, amount, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.IssueIn, user: UserToken, db: Session) -> tuple[Voucher, bool | None]:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(data.date_))
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
@@ -135,33 +119,11 @@ def save_inventories(
batch = db.execute(select(Batch).where(Batch.id == data_item.batch.id_)).scalar_one()
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
and_(ProductVersion.product_id == batch.sku.product_id, _pv_active(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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == batch.sku_id, _sv_active(voucher.date_)))
).scalar_one()
if batch_consumed and data_item.quantity > batch.quantity_remaining:
raise HTTPException(
@@ -223,48 +185,31 @@ def save_journals(
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["issue"])],
db: SessionDep,
data: schema_in.IssueIn = Depends(schema_in.IssueIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["issue"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item, batch_consumed = update_voucher(id_, data, user, db)
amount = update_inventories(item, data.inventories, batch_consumed, db)
check_duplicate_batches(item)
update_journals(item, data.source, data.destination, amount)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
# item: Voucher = db.execute(select(Voucher).where(Voucher.id == item.id)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item, batch_consumed = update_voucher(id_, data, user, db)
amount = update_inventories(item, data.inventories, batch_consumed, db)
check_duplicate_batches(item)
update_journals(item, data.source, data.destination, amount)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
db.commit()
report_date(request, data.date_)
# item: Voucher = db.execute(select(Voucher).where(Voucher.id == item.id)).scalar_one()
return voucher_info(item, db)
def update_voucher(
id_: uuid.UUID, data: schema_in.IssueIn, user: UserToken, db: Session
) -> tuple[Voucher, bool | None]:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(data.date_))
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
@@ -344,33 +289,11 @@ def update_inventories(
)
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_,
),
)
and_(ProductVersion.product_id == batch.sku.product_id, _pv_active(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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == batch.sku_id, _sv_active(voucher.date_)))
).scalar_one()
if index is not None:
new_inventory = inventories.pop(index)
@@ -433,54 +356,36 @@ def update_journals(
def refresh_voucher(id_: uuid.UUID, batch_id: uuid.UUID, db: Session) -> None:
try:
db.execute(update(Voucher).where(Voucher.id == id_).values(last_edit_date=datetime.now(UTC)))
batch = db.execute(select(Batch).where(Batch.id == batch_id)).scalar_one()
db.execute(
update(Inventory)
.where(Inventory.voucher_id == id_, Inventory.batch_id == batch_id)
.values(rate=batch.rate, tax=batch.tax, discount=batch.discount)
)
amount = round(
db.execute(select(func.sum(Inventory.amount)).where(Inventory.voucher_id == id_)).scalar_one(), 2
)
db.execute(update(Journal).where(Journal.voucher_id == id_).values(amount=amount))
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
db.execute(update(Voucher).where(Voucher.id == id_).values(last_edit_date=datetime.now(UTC)))
batch = db.execute(select(Batch).where(Batch.id == batch_id)).scalar_one()
db.execute(
update(Inventory)
.where(Inventory.voucher_id == id_, Inventory.batch_id == batch_id)
.values(rate=batch.rate, tax=batch.tax, discount=batch.discount)
)
amount = round(db.execute(select(func.sum(Inventory.amount)).where(Inventory.voucher_id == id_)).scalar_one(), 2)
db.execute(update(Journal).where(Journal.voucher_id == id_).values(amount=amount))
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["issue"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["issue"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
date: str | None = None,
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["issue"])],
db: SessionDep,
source: uuid.UUID | None = None,
destination: uuid.UUID | None = None,
user: UserToken = Security(get_user, scopes=["issue"]),
) -> output.Voucher:
date_ = date or get_date(request.session)
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.ISSUE) # type: ignore[arg-type]
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.ISSUE)
if source:
additional_info.source = CostCentreLink(id_=source, name="")
if destination:
additional_info.destination = CostCentreLink(id_=destination, name="")
with SessionFuture() as db:
return blank_voucher(additional_info, db)
return blank_voucher(additional_info, db)
+9 -8
View File
@@ -1,15 +1,17 @@
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..schemas.issue_grid_item import IssueGridItem
from ..schemas.user import UserToken
from .reports import path_date
router = APIRouter()
@@ -17,12 +19,11 @@ router = APIRouter()
@router.get("/{date_}", response_model=list[IssueGridItem])
def grid_date(
date_: str,
user: UserToken = Security(get_user, scopes=["issue"]),
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["issue"])],
db: SessionDep,
) -> list[IssueGridItem]:
date_obj = datetime.strptime(date_, "%d-%b-%Y")
with SessionFuture() as db:
return get_grid(date_obj, db)
return get_grid(date_, db)
def get_grid(date_: date, db: Session) -> list[IssueGridItem]:
+31 -55
View File
@@ -1,15 +1,14 @@
import uuid
from datetime import UTC, date, datetime
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import distinct, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.journal import Journal
from ..models.validations import check_journals_are_valid
@@ -20,8 +19,9 @@ from ..schemas import voucher as output
from ..schemas.account import AccountLink
from ..schemas.blank_voucher_info import BlankVoucherInfo
from ..schemas.user import UserToken
from . import get_lock_info, session_or_query_date
from . import get_lock_info
from .db_image import save_files, update_files
from .reports import report_date
from .tag import save_tags, update_tags
from .voucher import blank_voucher, check_voucher_edit_allowed, voucher_info
@@ -32,27 +32,21 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["journal"])],
db: SessionDep,
data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["journal"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = save(data, user, db)
db.flush()
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = save(data, user, db)
db.flush()
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher:
@@ -94,29 +88,19 @@ def save(data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher:
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["journal"])],
db: SessionDep,
data: schema_in.JournalIn = Depends(schema_in.JournalIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["journal"]),
) -> output.Voucher:
with SessionFuture() as db:
try:
item: Voucher = update_voucher(id_, data, user, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return voucher_info(item, db)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
except Exception:
db.rollback()
raise
item: Voucher = update_voucher(id_, data, user, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
return voucher_info(item, db)
def update_voucher(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, db: Session) -> Voucher:
@@ -174,26 +158,19 @@ def update_voucher(id_: uuid.UUID, data: schema_in.JournalIn, user: UserToken, d
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["journal"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["journal"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["journal"])],
db: SessionDep,
a: uuid.UUID | None = None,
date_: date = Depends(session_or_query_date),
user: UserToken = Security(get_user, scopes=["journal"]),
) -> output.Voucher:
if request.scope.get("path") == "/api/payment":
type_ = VoucherType.PAYMENT
@@ -205,5 +182,4 @@ def show_blank(
additional_info = BlankVoucherInfo(date_=date_, type_=type_)
if a:
additional_info.account = AccountLink(id_=a)
with SessionFuture() as db:
return blank_voucher(additional_info, db)
return blank_voucher(additional_info, db)
+19 -25
View File
@@ -1,11 +1,13 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_type import AccountType
from ..models.db_setting import DbSetting
from ..models.setting_type import SettingType
@@ -24,46 +26,38 @@ router = APIRouter()
@router.post("", response_model=list[LockInformation])
def post(
data_in: LockInformation,
user: UserToken = Security(get_user, scopes=["lock-date"]),
data_in: LockInformation, user: Annotated[UserToken, Security(get_user, scopes=["lock-date"])], db: SessionDep
) -> list[LockInformation]:
id_ = uuid.uuid4()
data_in.id_ = id_
data_dict = data_in.dict()
data_dict["voucher_types"] = [item["id_"] for item in data_dict["voucher_types"]]
data_dict["account_types"] = [item["id_"] for item in data_dict["account_types"]]
with SessionFuture() as db:
lock_date = DbSetting(
id_=id_,
name=f"Lock Info {str(id_)}",
setting_type=SettingType.VOUCHER_LOCK,
data=data_in.dict(),
valid_from=data_in.valid_from,
valid_till=data_in.valid_till,
)
db.add(lock_date)
db.commit()
lock_date = DbSetting(
id_=id_,
name=f"Lock Info {str(id_)}",
setting_type=SettingType.VOUCHER_LOCK,
data=data_in.dict(),
valid_from=data_in.valid_from,
valid_till=data_in.valid_till,
)
db.add(lock_date)
db.commit()
return get_info(db)
@router.delete("/{id_}", response_model=list[LockInformation])
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["lock-date"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["lock-date"])], db: SessionDep
) -> list[LockInformation]:
with SessionFuture() as db:
db.execute(delete(DbSetting).where(DbSetting.id == id_))
db.commit()
db.execute(delete(DbSetting).where(DbSetting.id == id_))
db.commit()
return get_info(db)
@router.get("", response_model=list[LockInformation])
def get(
user: UserToken = Security(get_user),
) -> list[LockInformation]:
with SessionFuture() as db:
return get_info(db)
def get(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[LockInformation]:
return get_info(db)
def get_info(db: Session) -> list[LockInformation]:
+57 -56
View File
@@ -1,4 +1,5 @@
from datetime import UTC, datetime, timedelta
from typing import Annotated
from fastapi import (
APIRouter,
@@ -23,7 +24,7 @@ from ..core.security import (
create_access_token,
get_current_active_user,
)
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.client import Client
from ..models.login_history import LoginHistory
from ..schemas.user import UserToken
@@ -35,68 +36,68 @@ router = APIRouter()
@router.post("/token", response_model=Token)
async def login_for_access_token(
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: SessionDep,
client_id: int = Cookie(None),
otp: int = Form(None),
) -> Token | JSONResponse:
with SessionFuture() as db:
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
allowed, client = client_allowed(user, client_id, otp, db)
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
allowed, client = client_allowed(user, client_id, otp, db)
db.flush()
if allowed:
history = LoginHistory(user.id, client.id)
db.add(history)
db.flush()
if allowed:
history = LoginHistory(user.id, client.id)
db.add(history)
db.flush()
db.execute(
delete(LoginHistory)
.where(
or_(
LoginHistory.date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=30),
LoginHistory.client_id.in_(
select(Client.id).where(
Client.creation_date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=3),
Client.enabled == False, # noqa: E712
)
),
)
)
.execution_options(synchronize_session=False)
)
db.execute(
delete(Client).where(
Client.creation_date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=3),
Client.enabled == False, # noqa: E712
db.execute(
delete(LoginHistory)
.where(
or_(
LoginHistory.date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=30),
LoginHistory.client_id.in_(
select(Client.id).where(
Client.creation_date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=3),
Client.enabled == False, # noqa: E712
)
),
)
)
db.commit()
response.set_cookie(key="client_id", value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
if not allowed:
not_allowed_response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},
content={"detail": "Client is not registered"},
)
not_allowed_response.set_cookie(key="client_id", value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
return not_allowed_response
access_token_expires = timedelta(minutes=settings.JWT_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={
"sub": user.name,
"scopes": ["authenticated"]
+ list(set([p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions])), # noqa: W503
"userId": str(user.id),
"lockedOut": user.locked_out,
"ver": __version__.__version__,
},
expires_delta=access_token_expires,
.execution_options(synchronize_session=False)
)
db.execute(
delete(Client).where(
Client.creation_date < datetime.now(UTC).replace(tzinfo=None) - timedelta(days=3),
Client.enabled == False, # noqa: E712
)
return Token(access_token=access_token, token_type="bearer")
)
db.commit()
response.set_cookie(key="client_id", value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
if not allowed:
not_allowed_response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},
content={"detail": "Client is not registered"},
)
not_allowed_response.set_cookie(key="client_id", value=str(client.code), max_age=10 * 365 * 24 * 60 * 60)
return not_allowed_response
access_token_expires = timedelta(minutes=settings.JWT_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={
"sub": user.name,
"scopes": ["authenticated"]
+ list({p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions}), # noqa: W503
"userId": str(user.id),
"lockedOut": user.locked_out,
"ver": __version__.__version__,
},
expires_delta=access_token_expires,
)
return Token(access_token=access_token, token_type="bearer")
@router.post("/refresh", response_model=Token)
+24 -27
View File
@@ -1,9 +1,11 @@
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.db_setting import DbSetting
from ..models.setting_type import SettingType
from ..models.user import User
@@ -15,38 +17,33 @@ router = APIRouter()
@router.get("", response_model=Maintenance)
def get_maintenance(
user: UserToken = Security(get_user),
) -> Maintenance:
with SessionFuture() as db:
data = db.execute(select(DbSetting).where(DbSetting.name == "Maintenance")).scalars().one_or_none()
return info(data, db)
def get_maintenance(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> Maintenance:
data = db.execute(select(DbSetting).where(DbSetting.name == "Maintenance")).scalars().one_or_none()
return info(data, db)
@router.post("", response_model=Maintenance)
def set_maintenance(
data: Maintenance,
user: UserToken = Security(get_user, scopes=["maintenance"]),
data: Maintenance, user: Annotated[UserToken, Security(get_user, scopes=["maintenance"])], db: SessionDep
) -> Maintenance:
with SessionFuture() as db:
maintenance: DbSetting | None = db.execute(
select(DbSetting).where(DbSetting.name == "Maintenance")
).scalar_one_or_none()
if maintenance is None:
if data.enabled:
maintenance = DbSetting(
name="Maintenance", setting_type=SettingType.MAINTENANCE_MODE, data={"id": user.id_}
)
db.add(maintenance)
maintenance: DbSetting | None = db.execute(
select(DbSetting).where(DbSetting.name == "Maintenance")
).scalar_one_or_none()
if maintenance is None:
if data.enabled:
maintenance = DbSetting(
name="Maintenance", setting_type=SettingType.MAINTENANCE_MODE, data={"id": user.id_}
)
db.add(maintenance)
else:
if not data.enabled:
db.delete(maintenance)
maintenance = None
else:
if not data.enabled:
db.delete(maintenance)
maintenance = None
else:
if maintenance.data["id"] != user.id_:
maintenance.data = {"id": user.id_}
db.commit()
return info(maintenance, db)
if maintenance.data["id"] != user.id_:
maintenance.data = {"id": user.id_}
db.commit()
return info(maintenance, db)
def info(data: DbSetting | None, db: Session) -> Maintenance:
+34 -56
View File
@@ -1,15 +1,17 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Depends, Security
from sqlalchemy import delete, desc, select
from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..core.session import get_finish_date, get_start_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.period import Period
from ..schemas import period as schemas
from ..schemas.user import UserToken
from .reports import report_finish_date, report_start_date
router = APIRouter()
@@ -17,77 +19,60 @@ router = APIRouter()
@router.post("", response_model=schemas.Period)
def save(
data: schemas.PeriodIn,
user: UserToken = Security(get_user, scopes=["recipes"]),
data: schemas.PeriodIn, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.Period:
try:
with SessionFuture() as db:
item = Period(valid_from=data.valid_from, valid_till=data.valid_till)
db.add(item)
db.commit()
return period_info(item)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = Period(valid_from=data.valid_from, valid_till=data.valid_till)
db.add(item)
db.commit()
return period_info(item)
@router.put("/{id_}", response_model=schemas.Period)
def update_route(
id_: uuid.UUID,
data: schemas.PeriodIn,
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
db: SessionDep,
) -> schemas.Period:
try:
with SessionFuture() as db:
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
item.valid_from = data.valid_from
item.valid_till = data.valid_till
db.commit()
return period_info(item)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
item.valid_from = data.valid_from
item.valid_till = data.valid_till
db.commit()
return period_info(item)
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
with SessionFuture() as db:
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
db.execute(delete(Period).where(Period.id == item.id))
db.commit()
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
db.execute(delete(Period).where(Period.id == item.id))
db.commit()
@router.get("", response_model=schemas.PeriodIn)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["recipes"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
) -> schemas.PeriodIn:
return period_blank(request)
return schemas.PeriodIn(
valid_from=start_date,
valid_till=finish_date,
)
@router.get("/list", response_model=list[schemas.Period])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Period]:
with SessionFuture() as db:
return [
period_info(item) for item in db.execute(select(Period).order_by(desc(Period.valid_from))).scalars().all()
]
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.Period]:
return [period_info(item) for item in db.execute(select(Period).order_by(desc(Period.valid_from))).scalars().all()]
@router.get("/{id_}", response_model=schemas.Period)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.Period:
with SessionFuture() as db:
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
return period_info(item)
item: Period = db.execute(select(Period).where(Period.id == id_)).scalar_one()
return period_info(item)
def period_info(item: Period) -> schemas.Period:
@@ -96,10 +81,3 @@ def period_info(item: Period) -> schemas.Period:
valid_from=item.valid_from,
valid_till=item.valid_till,
)
def period_blank(request: Request) -> schemas.PeriodIn:
return schemas.PeriodIn(
valid_from=get_start_date(request.session), # type: ignore[arg-type]
valid_till=get_finish_date(request.session), # type: ignore[arg-type]
)
+323 -479
View File
@@ -3,14 +3,14 @@ import uuid
from collections.abc import Sequence
from datetime import date, timedelta
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import and_, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.product import Product
from ..models.product_group import ProductGroup
@@ -24,7 +24,7 @@ from ..schemas.product_group import ProductGroupLink
from ..schemas.product_sku import ProductSku
from ..schemas.stock_keeping_unit import StockKeepingUnit as SkuSchema
from ..schemas.user import UserToken
from . import effective_date
from . import _pv_active, _sv_active, _sv_onclause, effective_date
router = APIRouter()
@@ -32,541 +32,385 @@ router = APIRouter()
@router.post("", response_model=schemas.Product)
def save(
data: schemas.ProductIn,
user: UserToken = Security(get_user, scopes=["products"]),
data: schemas.ProductIn, user: Annotated[UserToken, Security(get_user, scopes=["products"])], db: SessionDep
) -> schemas.Product:
try:
with SessionFuture() as db:
product = Product()
db.add(product)
db.flush() # So product.id is available
product = Product()
db.add(product)
db.flush() # So product.id is available
version = ProductVersion(
product_id=product.id,
name=data.name,
description=data.description,
fraction_units=data.fraction_units,
product_group_id=data.product_group.id_,
account_id=Account.all_purchases(),
is_purchased=data.is_purchased,
is_sold=data.is_sold,
allergen=data.allergen,
protein=data.protein,
carbohydrate=data.carbohydrate,
total_sugar=data.total_sugar,
added_sugar=data.added_sugar,
total_fat=data.total_fat,
saturated_fat=data.saturated_fat,
trans_fat=data.trans_fat,
cholestrol=data.cholestrol,
sodium=data.sodium,
msnf=data.msnf,
other_solids=data.other_solids,
total_solids=data.total_solids,
water=data.water,
valid_from=None,
valid_till=None,
)
db.add(version)
version = ProductVersion(
product_id=product.id,
name=data.name,
description=data.description,
fraction_units=data.fraction_units,
product_group_id=data.product_group.id_,
account_id=Account.all_purchases(),
is_purchased=data.is_purchased,
is_sold=data.is_sold,
allergen=data.allergen,
protein=data.protein,
carbohydrate=data.carbohydrate,
total_sugar=data.total_sugar,
added_sugar=data.added_sugar,
total_fat=data.total_fat,
saturated_fat=data.saturated_fat,
trans_fat=data.trans_fat,
cholestrol=data.cholestrol,
sodium=data.sodium,
msnf=data.msnf,
other_solids=data.other_solids,
total_solids=data.total_solids,
water=data.water,
valid_from=None,
valid_till=None,
)
db.add(version)
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
for sku in data.skus:
s = StockKeepingUnit(product=product)
product.skus.append(s)
db.add(s)
db.flush() # So sku.id is available
sver = SkuVersion(
units=sku.units,
fraction=round(sku.fraction, 5),
product_yield=round(sku.product_yield, 5),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
sku=s,
)
s.versions.append(sver)
db.add(sver)
db.commit()
db.refresh(product)
return product_info(version, [s.versions[0] for s in product.skus])
except SQLAlchemyError as e:
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
for sku in data.skus:
s = StockKeepingUnit(product=product)
product.skus.append(s)
db.add(s)
db.flush() # So sku.id is available
sver = SkuVersion(
units=sku.units,
fraction=round(sku.fraction, 5),
product_yield=round(sku.product_yield, 5),
cost_price=round(sku.cost_price, 2),
sale_price=round(sku.sale_price, 2),
sku=s,
)
s.versions.append(sver)
db.add(sver)
db.commit()
db.refresh(product)
return product_info(version, [s.versions[0] for s in product.skus])
@router.put("/{id_}", response_model=schemas.Product)
def update_route(
id_: uuid.UUID,
data: schemas.ProductIn,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep,
) -> schemas.Product:
try:
with SessionFuture() as db:
version: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one()
if version.product.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{version.name} is a fixture and cannot be edited or deleted.",
)
changed = (
data.name != version.name
or data.description != version.description
or data.fraction_units != version.fraction_units
or data.is_purchased != version.is_purchased
or data.is_sold != version.is_sold
or data.allergen != version.allergen
or Decimal(data.protein).quantize(Decimal("0.00001")) != version.protein
or Decimal(data.carbohydrate).quantize(Decimal("0.00001")) != version.carbohydrate
or Decimal(data.total_sugar).quantize(Decimal("0.00001")) != version.total_sugar
or Decimal(data.added_sugar).quantize(Decimal("0.00001")) != version.added_sugar
or Decimal(data.total_fat).quantize(Decimal("0.00001")) != version.total_fat
or Decimal(data.saturated_fat).quantize(Decimal("0.00001")) != version.saturated_fat
or Decimal(data.trans_fat).quantize(Decimal("0.00001")) != version.trans_fat
or Decimal(data.cholestrol).quantize(Decimal("0.00001")) != version.cholestrol
or Decimal(data.sodium).quantize(Decimal("0.00001")) != version.sodium
or Decimal(data.msnf).quantize(Decimal("0.00001")) != version.msnf
or Decimal(data.other_solids).quantize(Decimal("0.00001")) != version.other_solids
or Decimal(data.total_solids).quantize(Decimal("0.00001")) != version.total_solids
or Decimal(data.water).quantize(Decimal("0.00001")) != version.water
or version.product_group_id != data.product_group.id_
or version.account_id != Account.all_purchases()
)
if changed:
new_version = (
version if version.valid_from == date_ else ProductVersion(product_id=id_, valid_from=date_)
)
new_version.handle = ProductVersion.slugify(data.name)
new_version.name = data.name
new_version.description = data.description
new_version.fraction_units = data.fraction_units
new_version.product_group_id = data.product_group.id_
new_version.account_id = Account.all_purchases()
new_version.is_purchased = data.is_purchased
new_version.is_sold = data.is_sold
new_version.allergen = data.allergen
new_version.protein = data.protein
new_version.carbohydrate = data.carbohydrate
new_version.total_sugar = data.total_sugar
new_version.added_sugar = data.added_sugar
new_version.total_fat = data.total_fat
new_version.saturated_fat = data.saturated_fat
new_version.trans_fat = data.trans_fat
new_version.cholestrol = data.cholestrol
new_version.sodium = data.sodium
new_version.msnf = data.msnf
new_version.other_solids = data.other_solids
new_version.total_solids = data.total_solids
new_version.water = data.water
if version.valid_from != date_:
version.valid_till = date_ - timedelta(days=1)
version.product.versions.append(new_version)
db.add(new_version)
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
old_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
)
for i in range(len(old_skus), 0, -1):
sku: SkuVersion = old_skus[i - 1]
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None)
if index is not None:
new_data_sku = data.skus.pop(index)
sku_changed = (
new_data_sku.units != sku.units
or Decimal(new_data_sku.fraction).quantize(Decimal("0.00001")) != sku.fraction
or Decimal(new_data_sku.product_yield).quantize(Decimal("0.00001")) != sku.product_yield
or Decimal(new_data_sku.cost_price).quantize(Decimal("0.01")) != sku.cost_price
or Decimal(new_data_sku.sale_price).quantize(Decimal("0.01")) != sku.sale_price
)
print(f"SKU Changed: {sku_changed} for {sku.id}")
if sku_changed:
new_sku = (
sku
if sku.valid_from == date_
else SkuVersion(
sku_id=sku.sku_id,
valid_from=date_,
)
)
new_sku.units = new_data_sku.units
new_sku.fraction = round(new_data_sku.fraction, 5)
new_sku.product_yield = round(new_data_sku.product_yield, 5)
new_sku.cost_price = round(new_data_sku.cost_price, 2)
new_sku.sale_price = round(new_data_sku.sale_price, 2)
if sku.valid_from != date_:
sku.valid_till = date_ - timedelta(days=1)
db.add(new_sku)
else:
sku.valid_till = date_ - timedelta(days=1)
for data_sku in data.skus:
new_s = StockKeepingUnit(product_id=version.product_id)
version.product.skus.append(new_s)
db.add(new_s)
db.flush() # So sku.id is available
new_sku = SkuVersion(
units=data_sku.units,
fraction=round(data_sku.fraction, 5),
product_yield=round(data_sku.product_yield, 5),
cost_price=round(data_sku.cost_price, 2),
sale_price=round(data_sku.sale_price, 2),
sku=new_s,
)
db.add(new_sku)
current_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
)
db.commit()
return product_info(version, current_skus)
except SQLAlchemyError as e:
version: ProductVersion = db.execute(
select(ProductVersion).where(and_(ProductVersion.product_id == id_, _pv_active(date_)))
).scalar_one()
if version.product.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail=f"{version.name} is a fixture and cannot be edited or deleted.",
)
changed = (
data.name != version.name
or data.description != version.description
or data.fraction_units != version.fraction_units
or data.is_purchased != version.is_purchased
or data.is_sold != version.is_sold
or data.allergen != version.allergen
or Decimal(data.protein).quantize(Decimal("0.00001")) != version.protein
or Decimal(data.carbohydrate).quantize(Decimal("0.00001")) != version.carbohydrate
or Decimal(data.total_sugar).quantize(Decimal("0.00001")) != version.total_sugar
or Decimal(data.added_sugar).quantize(Decimal("0.00001")) != version.added_sugar
or Decimal(data.total_fat).quantize(Decimal("0.00001")) != version.total_fat
or Decimal(data.saturated_fat).quantize(Decimal("0.00001")) != version.saturated_fat
or Decimal(data.trans_fat).quantize(Decimal("0.00001")) != version.trans_fat
or Decimal(data.cholestrol).quantize(Decimal("0.00001")) != version.cholestrol
or Decimal(data.sodium).quantize(Decimal("0.00001")) != version.sodium
or Decimal(data.msnf).quantize(Decimal("0.00001")) != version.msnf
or Decimal(data.other_solids).quantize(Decimal("0.00001")) != version.other_solids
or Decimal(data.total_solids).quantize(Decimal("0.00001")) != version.total_solids
or Decimal(data.water).quantize(Decimal("0.00001")) != version.water
or version.product_group_id != data.product_group.id_
or version.account_id != Account.all_purchases()
)
if changed:
new_version = version if version.valid_from == date_ else ProductVersion(product_id=id_, valid_from=date_)
new_version.handle = ProductVersion.slugify(data.name)
new_version.name = data.name
new_version.description = data.description
new_version.fraction_units = data.fraction_units
new_version.product_group_id = data.product_group.id_
new_version.account_id = Account.all_purchases()
new_version.is_purchased = data.is_purchased
new_version.is_sold = data.is_sold
new_version.allergen = data.allergen
new_version.protein = data.protein
new_version.carbohydrate = data.carbohydrate
new_version.total_sugar = data.total_sugar
new_version.added_sugar = data.added_sugar
new_version.total_fat = data.total_fat
new_version.saturated_fat = data.saturated_fat
new_version.trans_fat = data.trans_fat
new_version.cholestrol = data.cholestrol
new_version.sodium = data.sodium
new_version.msnf = data.msnf
new_version.other_solids = data.other_solids
new_version.total_solids = data.total_solids
new_version.water = data.water
if version.valid_from != date_:
version.valid_till = date_ - timedelta(days=1)
version.product.versions.append(new_version)
db.add(new_version)
if not len(data.skus):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough stock keeping units.",
)
old_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(and_(StockKeepingUnit.product_id == id_, _sv_active(date_)))
)
.scalars()
.all()
)
for i in range(len(old_skus), 0, -1):
sku: SkuVersion = old_skus[i - 1]
index = next((idx for (idx, d) in enumerate(data.skus) if d.id_ == sku.id), None)
if index is not None:
new_data_sku = data.skus.pop(index)
sku_changed = (
new_data_sku.units != sku.units
or Decimal(new_data_sku.fraction).quantize(Decimal("0.00001")) != sku.fraction
or Decimal(new_data_sku.product_yield).quantize(Decimal("0.00001")) != sku.product_yield
or Decimal(new_data_sku.cost_price).quantize(Decimal("0.01")) != sku.cost_price
or Decimal(new_data_sku.sale_price).quantize(Decimal("0.01")) != sku.sale_price
)
if sku_changed:
new_sku = (
sku
if sku.valid_from == date_
else SkuVersion(
sku_id=sku.sku_id,
valid_from=date_,
)
)
new_sku.units = new_data_sku.units
new_sku.fraction = round(new_data_sku.fraction, 5)
new_sku.product_yield = round(new_data_sku.product_yield, 5)
new_sku.cost_price = round(new_data_sku.cost_price, 2)
new_sku.sale_price = round(new_data_sku.sale_price, 2)
if sku.valid_from != date_:
sku.valid_till = date_ - timedelta(days=1)
db.add(new_sku)
else:
sku.valid_till = date_ - timedelta(days=1)
for data_sku in data.skus:
new_s = StockKeepingUnit(product_id=version.product_id)
version.product.skus.append(new_s)
db.add(new_s)
db.flush() # So sku.id is available
new_sku = SkuVersion(
units=data_sku.units,
fraction=round(data_sku.fraction, 5),
product_yield=round(data_sku.product_yield, 5),
cost_price=round(data_sku.cost_price, 2),
sale_price=round(data_sku.sale_price, 2),
sku=new_s,
)
db.add(new_sku)
current_skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(and_(StockKeepingUnit.product_id == id_, _sv_active(date_)))
)
.scalars()
.all()
)
db.commit()
return product_info(version, current_skus)
@router.delete("/{id_}", response_model=schemas.ProductBlank)
def delete_route(
id_: uuid.UUID,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep,
) -> schemas.ProductBlank:
with SessionFuture() as db:
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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_,
),
)
)
).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()
item: Product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="This product is a fixture and cannot be edited or deleted.",
)
valid_till = date_ - timedelta(days=1)
version.valid_till = valid_till
for sku in skus:
sku.valid_till = valid_till
db.commit()
return product_blank()
version: ProductVersion = db.execute(
select(ProductVersion).where(and_(ProductVersion.product_id == id_, _pv_active(date_)))
).scalar_one()
skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(and_(StockKeepingUnit.product_id == id_, _sv_active(date_)))
)
.scalars()
.all()
)
valid_till = date_ - timedelta(days=1)
version.valid_till = valid_till
for sku in skus:
sku.valid_till = valid_till
db.commit()
return product_blank()
@router.get("", response_model=schemas.ProductBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["products"]),
user: Annotated[UserToken, Security(get_user, scopes=["products"])],
) -> schemas.ProductBlank:
return product_blank()
@router.get("/list", response_model=list[schemas.Product])
def show_list(date_: date = Depends(effective_date), user: UserToken = Depends(get_user)) -> list[schemas.Product]:
with SessionFuture() as db:
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
return [
product_info(item, [s.versions[0] for s in item.product.skus])
for item in db.execute(
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_,
),
)
)
.order_by(ProductGroup.name)
.order_by(ProductVersion.name)
.options(
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
)
.unique()
.scalars()
.all()
]
def show_list(
date_: Annotated[date, Depends(effective_date)], user: Annotated[UserToken, Security(get_user)], db: SessionDep
) -> list[schemas.Product]:
@router.get("/q-sku", response_model=list[ProductSku])
async def show_term_sku(
q: str | None = None, # Query
a: bool | None = None, # Active
p: bool | None = None, # Is Purchased?
v: uuid.UUID | None = None, # Vendor
date_: date = Depends(effective_date), # Date
current_user: UserToken = Depends(get_user),
) -> list[ProductSku]:
list_ = []
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_,
),
)
query_ = (
return [
product_info(item, [s.versions[0] for s in item.product.skus])
for item in db.execute(
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_,
),
)
.join(SkuVersion, onclause=_sv_onclause(date_))
.where(and_(_pv_active(date_)))
.order_by(ProductGroup.name)
.order_by(ProductVersion.name)
.options(
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
)
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(
or_(ProductVersion.name.ilike(f"%{sub}%"), SkuVersion.units.ilike(f"%{sub}%"))
)
query_ = query_.order_by(ProductVersion.name)
query_ = query_.options(
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
.unique()
.scalars()
.all()
]
for item in db.execute(query_).unique().scalars().all():
for sku in [sku.versions[0] for sku in item.product.skus]:
rc_price = get_rc_price(item.id, date_, v, db)
list_.append(
ProductSku(
id_=sku.sku_id,
name=f"{item.name} ({sku.units})",
fraction_units=item.fraction_units,
cost_price=sku.cost_price if rc_price is None else rc_price,
sale_price=sku.sale_price,
is_rate_contracted=rc_price is not None,
)
@router.get("/q-sku", response_model=list[ProductSku])
async def show_term_sku(
date_: Annotated[date, Depends(effective_date)], # Date
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
q: str | None = None, # Query
a: bool | None = None, # Active # noqa: ARG001
p: bool | None = None, # Is Purchased?
v: uuid.UUID | None = None, # Vendor
) -> list[ProductSku]:
list_ = []
query_ = (
select(ProductVersion)
.join(ProductVersion.product)
.join(ProductVersion.product_group)
.join(ProductVersion.product)
.join(Product.skus)
.join(SkuVersion, onclause=_sv_onclause(date_))
.where(_pv_active(date_))
)
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(or_(ProductVersion.name.ilike(f"%{sub}%"), SkuVersion.units.ilike(f"%{sub}%")))
query_ = query_.order_by(ProductVersion.name)
query_ = query_.options(
contains_eager(ProductVersion.product, Product.skus, StockKeepingUnit.versions),
contains_eager(ProductVersion.product_group),
)
for item in db.execute(query_).unique().scalars().all():
for sku in [sku.versions[0] for sku in item.product.skus]:
rc_price = get_rc_price(item.id, date_, v, db)
list_.append(
ProductSku(
id_=sku.sku_id,
name=f"{item.name} ({sku.units})",
fraction_units=item.fraction_units,
cost_price=sku.cost_price if rc_price is None else rc_price,
sale_price=sku.sale_price,
is_rate_contracted=rc_price is not None,
)
)
return list_
@router.get("/q-product", response_model=list[ProductSku])
async def show_term_product(
date_: Annotated[date, Depends(effective_date)], # Date
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
q: str | None = None, # Query
a: bool | None = None, # Active
a: bool | None = None, # Active # noqa: ARG001
p: bool | None = None, # Is Purchased?
date_: date = Depends(effective_date), # Date
current_user: UserToken = Depends(get_user),
) -> list[ProductSku]:
list_ = []
with SessionFuture() as db:
query_ = select(ProductVersion).where(
and_(
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
query_ = select(ProductVersion).where(_pv_active(date_))
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(ProductVersion.name.ilike(f"%{sub}%"))
query_ = query_.order_by(ProductVersion.name)
for item in db.execute(query_).unique().scalars().all():
list_.append(
ProductSku(
id_=item.product_id,
name=item.name,
fraction_units=item.fraction_units,
cost_price=Decimal(0),
sale_price=Decimal(0),
is_rate_contracted=False,
)
)
# if a is not None:
# query_ = query_.filter(Product.is_active == a)
if p is not None:
query_ = query_.filter(ProductVersion.is_purchased == p)
if q is not None:
for sub in q.split():
if sub.strip() != "":
query_ = query_.filter(ProductVersion.name.ilike(f"%{sub}%"))
query_ = query_.order_by(ProductVersion.name)
for item in db.execute(query_).unique().scalars().all():
list_.append(
ProductSku(
id_=item.product_id,
name=item.name,
fraction_units=item.fraction_units,
cost_price=Decimal(0),
sale_price=Decimal(0),
is_rate_contracted=False,
)
)
return list_
@router.get("/{id_}", response_model=schemas.Product)
def show_id(
id_: uuid.UUID,
date_: date = Depends(effective_date),
user: UserToken = Security(get_user, scopes=["products"]),
date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep,
) -> schemas.Product:
with SessionFuture() as db:
version: ProductVersion = db.execute(
select(ProductVersion).where(
and_(
ProductVersion.product_id == id_,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
)
).scalar_one()
skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(
and_(
StockKeepingUnit.product_id == id_,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
)
)
.scalars()
.all()
version: ProductVersion = db.execute(
select(ProductVersion).where(and_(ProductVersion.product_id == id_, _pv_active(date_)))
).scalar_one()
skus = (
db.execute(
select(SkuVersion)
.join(SkuVersion.sku)
.options(contains_eager(SkuVersion.sku))
.where(and_(StockKeepingUnit.product_id == id_, _sv_active(date_)))
)
return product_info(version, skus)
.scalars()
.all()
)
return product_info(version, skus)
def product_info(version: ProductVersion, skus: Sequence[SkuVersion]) -> schemas.Product:
+47 -63
View File
@@ -1,11 +1,12 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, Security, status
from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.product_group import ProductGroup
from ..schemas import product_group as schemas
from ..schemas.user import UserToken
@@ -17,96 +18,79 @@ router = APIRouter()
@router.post("", response_model=schemas.ProductGroup)
def save(
data: schemas.ProductGroupIn,
user: UserToken = Security(get_user, scopes=["product-groups"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-groups"])],
db: SessionDep,
) -> schemas.ProductGroup:
try:
with SessionFuture() as db:
item = ProductGroup(name=data.name, nutritional=data.nutritional, ice_cream=data.ice_cream)
db.add(item)
db.commit()
return product_group_info(item)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = ProductGroup(name=data.name, nutritional=data.nutritional, ice_cream=data.ice_cream)
db.add(item)
db.commit()
return product_group_info(item)
@router.put("/{id_}", response_model=schemas.ProductGroup)
def update_route(
id_: uuid.UUID,
data: schemas.ProductGroupIn,
user: UserToken = Security(get_user, scopes=["product-groups"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-groups"])],
db: SessionDep,
) -> schemas.ProductGroup:
try:
with SessionFuture() as db:
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.nutritional = data.nutritional
item.ice_cream = data.ice_cream
db.commit()
return product_group_info(item)
except SQLAlchemyError as e:
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
item.nutritional = data.nutritional
item.ice_cream = data.ice_cream
db.commit()
return product_group_info(item)
@router.delete("/{id_}", response_model=schemas.ProductGroupBlank)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["product-groups"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["product-groups"])], db: SessionDep
) -> schemas.ProductGroupBlank:
with SessionFuture() as db:
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product Group not found",
)
elif item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
else:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Product Group deletion not implemented",
)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product Group not found",
)
elif item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
else:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Product Group deletion not implemented",
)
@router.get("", response_model=schemas.ProductGroupBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["product-groups"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-groups"])],
) -> schemas.ProductGroupBlank:
return product_group_blank()
@router.get("/list", response_model=list[schemas.ProductGroup])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.ProductGroup]:
with SessionFuture() as db:
return [
product_group_info(item)
for item in db.execute(select(ProductGroup).order_by(ProductGroup.name)).scalars().all()
]
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.ProductGroup]:
return [
product_group_info(item)
for item in db.execute(select(ProductGroup).order_by(ProductGroup.name)).scalars().all()
]
@router.get("/{id_}", response_model=schemas.ProductGroup)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["product-groups"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["product-groups"])], db: SessionDep
) -> schemas.ProductGroup:
with SessionFuture() as db:
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
return product_group_info(item)
item = db.execute(select(ProductGroup).where(ProductGroup.id == id_)).scalar_one()
return product_group_info(item)
def product_group_info(item: ProductGroup) -> schemas.ProductGroup:
+44 -139
View File
@@ -2,15 +2,14 @@ import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import and_, distinct, func, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.batch import Batch
from ..models.inventory import Inventory
@@ -30,9 +29,10 @@ from ..schemas.account import AccountLink
from ..schemas.blank_voucher_info import BlankVoucherInfo
from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken
from . import get_lock_info
from . import _pv_active, _pv_onclause, _sv_active, get_lock_info
from .batch_integrity import fix_single_batch_prices
from .db_image import save_files, update_files
from .reports import report_date
from .tag import save_tags, update_tags
from .voucher import (
blank_voucher,
@@ -48,29 +48,23 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["purchase"])],
db: SessionDep,
data: schema_in.PurchaseIn = Depends(schema_in.PurchaseIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["purchase"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = save(data, user, db)
save_inventories(item, data.vendor.id_, data.inventories, db)
check_duplicate_skus(item)
save_journals(item, data.vendor, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = save(data, user, db)
save_inventories(item, data.vendor.id_, data.inventories, db)
check_duplicate_skus(item)
save_journals(item, data.vendor, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
@@ -78,17 +72,7 @@ def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
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_,
),
)
and_(ProductVersion.product_id.in_(product_ids), _pv_active(data.date_))
)
account_types: list[int] = list(
db.execute(
@@ -152,19 +136,7 @@ def save_inventories(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
)
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_,
),
)
)
.where(and_(SkuVersion.sku_id == sku.id, _sv_active(voucher.date_)))
.values(cost_price=item.rate)
)
voucher.inventories.append(inventory)
@@ -178,17 +150,7 @@ def save_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
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_,
),
)
and_(ProductVersion.product_id == product_id, _pv_active(voucher.date_))
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id).where(AccountBase.id == product_account)
@@ -218,28 +180,22 @@ def save_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["purchase"])],
db: SessionDep,
data: schema_in.PurchaseIn = Depends(schema_in.PurchaseIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["purchase"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, db)
update_inventory(item, data.vendor.id_, data.inventories, db)
check_duplicate_skus(item)
update_journals(item, data.vendor, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = update_voucher(id_, data, user, db)
update_inventory(item, data.vendor.id_, data.inventories, db)
check_duplicate_skus(item)
update_journals(item, data.vendor, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
return voucher_info(item, db)
def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
@@ -248,17 +204,7 @@ def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken,
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_,
),
)
and_(ProductVersion.product_id.in_(product_ids), _pv_active(data.date_))
)
account_types: list[int] = list(
@@ -300,17 +246,7 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
)
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_,
),
)
and_(ProductVersion.product_id == item.batch.sku.product_id, _pv_active(voucher.date_))
)
).scalar_one()
if index is not None:
@@ -349,19 +285,7 @@ def update_inventory(voucher: Voucher, vendor_id: uuid.UUID, inventories: list[I
item.batch.tax = new_inventory.tax
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_,
),
)
)
.where(and_(SkuVersion.sku_id == sku.id, _sv_active(voucher.date_)))
.values(cost_price=new_inventory.rate)
)
db.flush()
@@ -387,21 +311,10 @@ def update_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for i_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(voucher.date_))
.join(Product.skus)
.where(StockKeepingUnit.id == i_item.batch.sku.id)
).one()
@@ -439,28 +352,20 @@ def update_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["purchase"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["purchase"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["purchase"]),
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["purchase"])],
db: SessionDep,
) -> output.Voucher:
additional_info = BlankVoucherInfo(date_=get_date(request.session), type_=VoucherType.PURCHASE) # type: ignore
with SessionFuture() as db:
return blank_voucher(additional_info, db)
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.PURCHASE)
return blank_voucher(additional_info, db)
def rate_contract_price(id_: uuid.UUID, vendor_id: uuid.UUID, date_: date, db: Session) -> Decimal | None:
+47 -157
View File
@@ -1,16 +1,15 @@
import uuid
from datetime import UTC, datetime
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, Security, status
from sqlalchemy import and_, distinct, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, contains_eager
from ..core.security import get_current_active_user as get_user
from ..core.session import get_date, set_date
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account_base import AccountBase
from ..models.batch import Batch
from ..models.inventory import Inventory
@@ -28,8 +27,9 @@ from ..schemas.account import AccountLink
from ..schemas.blank_voucher_info import BlankVoucherInfo
from ..schemas.inventory import Inventory as InventorySchema
from ..schemas.user import UserToken
from . import get_lock_info
from . import _pv_active, _pv_onclause, _sv_active, get_lock_info
from .db_image import save_files, update_files
from .reports import report_date
from .tag import save_tags, update_tags
from .voucher import (
blank_voucher,
@@ -45,46 +45,29 @@ router = APIRouter()
@router.post("", response_model=output.Voucher)
def save_route(
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["purchase-return"])],
db: SessionDep,
data: schema_in.PurchaseIn = Depends(schema_in.PurchaseIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["purchase-return"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = save(data, user, db)
save_inventories(item, data.inventories, db)
check_duplicate_batches(item)
save_journals(item, data.vendor, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
info = voucher_info(item, db)
return info
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = save(data, user, db)
save_inventories(item, data.inventories, db)
check_duplicate_batches(item)
save_journals(item, data.vendor, db)
check_journals_are_valid(item)
save_files(item.id, i, t, db)
save_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
info = voucher_info(item, db)
return info
def save(data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(data.date_))
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
@@ -126,33 +109,11 @@ def save_inventories(voucher: Voucher, inventories: list[InventorySchema], db: S
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_,
),
)
and_(ProductVersion.product_id == batch.sku.product_id, _pv_active(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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == batch.sku_id, _sv_active(voucher.date_)))
).scalar_one()
if d_item.quantity > batch.quantity_remaining:
@@ -184,21 +145,10 @@ def save_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for v_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(voucher.date_))
.join(Product.skus)
.where(StockKeepingUnit.id == v_item.batch.sku.id)
).one()
@@ -227,50 +177,30 @@ def save_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
def update_route(
id_: uuid.UUID,
request: Request,
user: Annotated[UserToken, Security(get_user, scopes=["purchase-return"])],
db: SessionDep,
data: schema_in.PurchaseIn = Depends(schema_in.PurchaseIn.load_form),
i: list[bytes] = File(None),
t: list[bytes] = File(None),
user: UserToken = Security(get_user, scopes=["purchase-return"]),
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = update_voucher(id_, data, user, db)
update_inventory(item, data.inventories, db)
check_duplicate_batches(item)
update_journals(item, data.vendor, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return voucher_info(item, db)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
except Exception:
db.rollback()
raise
item: Voucher = update_voucher(id_, data, user, db)
update_inventory(item, data.inventories, db)
check_duplicate_batches(item)
update_journals(item, data.vendor, db)
check_journals_are_valid(item)
update_files(item.id, data.files, i, t, db)
update_tags(item, data.tags, db)
db.commit()
report_date(request, data.date_)
return voucher_info(item, db)
def update_voucher(id_: uuid.UUID, data: schema_in.PurchaseIn, user: UserToken, db: Session) -> Voucher:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= data.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= data.date_,
),
)
product_accounts = (
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(data.date_))
.join(Product.skus)
.options(
contains_eager(ProductVersion.product).contains_eager(Product.versions),
@@ -317,33 +247,11 @@ def update_inventory(voucher: Voucher, new_inventories: list[InventorySchema], d
)
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_,
),
)
and_(ProductVersion.product_id == batch.sku.product_id, _pv_active(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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == batch.sku_id, _sv_active(voucher.date_)))
).scalar_one()
if index is not None:
new_inventory = new_inventories.pop(index)
@@ -374,17 +282,7 @@ def update_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
journals: dict[uuid.UUID, Journal] = {}
amount = Decimal(0)
for v_item in voucher.inventories:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
product_version_onclause = and_(ProductVersion.product_id == Product.id, _pv_active(voucher.date_))
account_id, cc_id = db.execute(
select(AccountBase.id, AccountBase.cost_centre_id)
.join(AccountBase.products)
@@ -426,25 +324,17 @@ def update_journals(voucher: Voucher, ven: AccountLink, db: Session) -> None:
@router.get("/{id_}", response_model=output.Voucher)
def get_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["purchase-return"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["purchase-return"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
return voucher_info(item, db)
@router.get("", response_model=output.Voucher)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["purchase-return"]),
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["purchase-return"])],
db: SessionDep,
) -> output.Voucher:
additional_info = BlankVoucherInfo(date_=get_date(request.session), type_=VoucherType.PURCHASE_RETURN) # type: ignore[arg-type]
with SessionFuture() as db:
return blank_voucher(additional_info, db)
additional_info = BlankVoucherInfo(date_=date_, type_=VoucherType.PURCHASE_RETURN)
return blank_voucher(additional_info, db)
+73 -112
View File
@@ -1,22 +1,14 @@
import uuid
from datetime import date
from typing import Any
from typing import Annotated
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import and_, delete, or_, select
from sqlalchemy.exc import SQLAlchemyError
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import and_, delete, select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import (
get_date,
get_finish_date,
get_start_date,
set_date,
set_period,
)
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.product_version import ProductVersion
from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem
@@ -29,6 +21,8 @@ from ..schemas.rate_contract import RateContractBlank, RateContractIn
from ..schemas.rate_contract_item import RateContractItem as RateContractItemSchema
from ..schemas.user import UserToken
from ..schemas.user_link import UserLink
from . import _pv_active, _sv_active
from .reports import report_date, report_finish_date, report_start_date
router = APIRouter()
@@ -38,29 +32,26 @@ router = APIRouter()
async def save(
data: RateContractIn,
request: Request,
user: UserToken = Security(get_user, scopes=["rate-contracts"]),
user: Annotated[UserToken, Security(get_user, scopes=["rate-contracts"])],
db: SessionDep,
) -> RateContractSchema:
try:
with SessionFuture() as db:
item = RateContract(
date_=data.date_,
vendor_id=data.vendor.id_,
valid_from=data.valid_from,
valid_till=data.valid_till,
narration=data.narration,
user_id=user.id_,
)
db.add(item)
add_items(item, data.items, db)
set_date(data.date_, request.session)
set_period(data.valid_from, data.valid_till, request.session)
db.commit()
return rate_contract_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = RateContract(
date_=data.date_,
vendor_id=data.vendor.id_,
valid_from=data.valid_from,
valid_till=data.valid_till,
narration=data.narration,
user_id=user.id_,
)
db.add(item)
add_items(item, data.items, db)
report_date(request, data.date_.strftime("%d-%b-%Y"))
if data.valid_from is not None:
report_start_date(request, data.valid_from.strftime("%d-%b-%Y"))
if data.valid_till is not None:
report_finish_date(request, data.valid_till.strftime("%d-%b-%Y"))
db.commit()
return rate_contract_info(item, db)
def add_items(rate_contract: RateContract, items: list[RateContractItemSchema], db: Session) -> None:
@@ -75,27 +66,24 @@ async def update_route(
id_: uuid.UUID,
data: RateContractIn,
request: Request,
user: UserToken = Security(get_user, scopes=["rate-contracts"]),
user: Annotated[UserToken, Security(get_user, scopes=["rate-contracts"])],
db: SessionDep,
) -> RateContractSchema:
try:
with SessionFuture() as db:
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
item.date_ = data.date_
item.vendor_id = data.vendor.id_
item.valid_from = data.valid_from
item.valid_till = data.valid_till
item.narration = data.narration
item.user_id = user.id_
update_items(item, data.items, db)
set_date(data.date_, request.session)
set_period(data.valid_from, data.valid_till, request.session)
db.commit()
return rate_contract_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
item.date_ = data.date_
item.vendor_id = data.vendor.id_
item.valid_from = data.valid_from
item.valid_till = data.valid_till
item.narration = data.narration
item.user_id = user.id_
update_items(item, data.items, db)
report_date(request, data.date_.strftime("%d-%b-%Y"))
if data.valid_from is not None:
report_start_date(request, data.valid_from.strftime("%d-%b-%Y"))
if data.valid_till is not None:
report_finish_date(request, data.valid_till.strftime("%d-%b-%Y"))
db.commit()
return rate_contract_info(item, db)
def update_items(rate_contract: RateContract, items: list[RateContractItemSchema], db: Session) -> None:
@@ -113,46 +101,53 @@ def update_items(rate_contract: RateContract, items: list[RateContractItemSchema
add_items(rate_contract, items, db)
@router.delete("/{id_}", response_model=RateContractBlank)
@router.delete("/{id_}", response_model=None)
async def delete_route(
id_: uuid.UUID,
request: Request,
user: UserToken = Security(get_user, scopes=["rate-contracts"]),
) -> RateContractBlank:
with SessionFuture() as db:
db.execute(delete(RateContractItem).where(RateContractItem.rate_contract_id == id_))
db.execute(delete(RateContract).where(RateContract.id == id_))
db.commit()
return rate_contract_blank(request.session)
user: Annotated[UserToken, Security(get_user, scopes=["rate-contracts"])],
db: SessionDep,
) -> None:
db.execute(delete(RateContractItem).where(RateContractItem.rate_contract_id == id_))
db.execute(delete(RateContract).where(RateContract.id == id_))
db.commit()
return None
@router.get("", response_model=RateContractBlank)
async def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["rate-contracts"]),
date_: Annotated[date, Depends(report_date)],
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["rate-contracts"])],
) -> RateContractBlank:
return rate_contract_blank(request.session)
return RateContractBlank(
date_=date_,
valid_from=start_date,
valid_till=finish_date,
narration="",
items=[],
)
@router.get("/list", response_model=list[RateContractSchema])
async def show_list(
user: UserToken = Security(get_user),
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
) -> list[RateContractSchema]:
with SessionFuture() as db:
return [
rate_contract_info(item, db)
for item in db.execute(select(RateContract).order_by(RateContract.date_)).scalars().all()
]
return [
rate_contract_info(item, db)
for item in db.execute(select(RateContract).order_by(RateContract.date_)).scalars().all()
]
@router.get("/{id_}", response_model=RateContractSchema)
async def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["rate-contracts"]),
user: Annotated[UserToken, Security(get_user, scopes=["rate-contracts"])],
db: SessionDep,
) -> RateContractSchema:
with SessionFuture() as db:
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
return rate_contract_info(item, db)
item: RateContract = db.execute(select(RateContract).where(RateContract.id == id_)).scalar_one()
return rate_contract_info(item, db)
def rate_contract_info(item: RateContract, db: Session) -> RateContractSchema:
@@ -179,44 +174,10 @@ def rate_contract_info(item: RateContract, db: Session) -> 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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == sku.id, _sv_active(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_,
),
)
)
select(ProductVersion.name).where(ProductVersion.product_id == sku.product_id, _pv_active(date_))
).scalar_one()
return f"{product_name} ({product_unit})"
def rate_contract_blank(session: dict[str, Any]) -> RateContractBlank:
return RateContractBlank(
date_=get_date(session), # type: ignore[arg-type]
valid_from=get_start_date(session), # type: ignore[arg-type]
valid_till=get_finish_date(session), # type: ignore[arg-type]
narration="",
items=[],
)
+28 -28
View File
@@ -1,15 +1,15 @@
# mypy: disable-error-code="arg-type"
import uuid
from datetime import date, datetime, timedelta
from datetime import date, timedelta
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy import and_, delete, distinct, func, select
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.account_base import AccountBase
from ..models.attendance import Attendance
@@ -27,6 +27,7 @@ from ..models.rate_contract_item import RateContractItem
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..schemas.user import UserToken
from .reports import path_date
router = APIRouter()
@@ -34,29 +35,28 @@ router = APIRouter()
@router.post("/{date_}", response_model=None)
def rebase(
date_: str,
user: UserToken = Security(get_user, scopes=["rebase"]),
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["rebase"])],
db: SessionDep,
) -> None:
with SessionFuture() as db:
date_obj = datetime.strptime(date_, "%d-%b-%Y")
voucher_l = opening_accounts(date_obj, user.id_, db)
voucher_b = opening_batches(date_obj, user.id_, db)
starred_vouchers = save_starred(date_obj, db)
db.flush()
delete_data(date_obj, starred_vouchers, db)
db.add(voucher_l)
for j in voucher_l.journals:
db.add(j)
db.flush()
db.add(voucher_b)
for j in voucher_b.journals:
db.add(j)
for i in voucher_b.inventories:
db.add(i)
db.flush()
cleanup_lint(date_obj, db)
# request.dbsession.execute('RESET statement_timeout;')
db.commit()
voucher_l = opening_accounts(date_, user.id_, db)
voucher_b = opening_batches(date_, user.id_, db)
starred_vouchers = save_starred(date_, db)
db.flush()
delete_data(date_, starred_vouchers, db)
db.add(voucher_l)
for j in voucher_l.journals:
db.add(j)
db.flush()
db.add(voucher_b)
for j in voucher_b.journals:
db.add(j)
for i in voucher_b.inventories:
db.add(i)
db.flush()
cleanup_lint(date_, db)
# request.dbsession.execute('RESET statement_timeout;')
db.commit()
def save_starred(date_: date, db: Session) -> list[uuid.UUID]:
@@ -121,7 +121,7 @@ def opening_accounts(date_: date, user_id: uuid.UUID, db: Session) -> Voucher:
Voucher.voucher_type != VoucherType.ISSUE,
)
.having(sum_func != 0)
.group_by(AccountBase)
.group_by(AccountBase.id)
).all()
dt = date_ - timedelta(days=1)
@@ -164,7 +164,7 @@ def opening_batches(date_: date, user_id: uuid.UUID, db: Session) -> Voucher:
.join(Voucher.journals)
.where(Voucher.date_ < date_, Journal.cost_centre_id == CostCentre.cost_centre_purchase())
.having(sum_func != 0)
.group_by(Batch)
.group_by(Batch.id)
).all()
dt = date_ - timedelta(days=1)
+229 -283
View File
@@ -7,17 +7,17 @@ from collections.abc import Sequence
from datetime import date
from decimal import Decimal
from io import BytesIO
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from fastapi import APIRouter, HTTPException, Security, status
from fastapi.responses import FileResponse, StreamingResponse
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, NamedStyle, PatternFill, Side
from sqlalchemy import and_, delete, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, aliased, contains_eager, joinedload
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.price import Price
from ..models.product import Product
from ..models.product_version import ProductVersion
@@ -32,6 +32,7 @@ from ..schemas import recipe_item as rischemas
from ..schemas.nutritional_information import NutritionalInformation
from ..schemas.product import ProductLink
from ..schemas.user import UserToken
from . import _pv_onclause, _sv_onclause
router = APIRouter()
@@ -40,113 +41,95 @@ router = APIRouter()
@router.post("", response_model=schemas.Recipe)
def save(
data: schemas.RecipeIn,
request: Request,
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
db: SessionDep,
) -> schemas.Recipe:
try:
with SessionFuture() as db:
recipe_sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == data.sku.id_)).scalar_one()
recipe_sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == data.sku.id_)).scalar_one()
if recipe_sku.product_id in set(i.product.id_ for i in data.items):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot also be an ingredient",
)
if len(data.items) == 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Recipe has no ingredients",
)
recipe = Recipe(
date_=data.date_,
source=data.source,
instructions=data.instructions,
garnishing=data.garnishing,
plating=data.plating,
notes=data.notes,
sku=recipe_sku,
recipe_yield=round(data.recipe_yield, 2),
)
for item in data.items:
product: Product = db.execute(select(Product).where(Product.id == item.product.id_)).scalar_one()
quantity = round(item.quantity, 2)
recipe.items.append(RecipeItem(product_id=product.id, quantity=quantity, description=item.description))
db.add(recipe)
for r_item in recipe.items:
r_item.recipe_id = recipe.id
db.add(r_item)
db.flush()
check_recursion(recipe_sku.product_id, set(), db)
db.commit()
return recipe_info(recipe)
except SQLAlchemyError as e:
if recipe_sku.product_id in {i.product.id_ for i in data.items}:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot also be an ingredient",
)
if len(data.items) == 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Recipe has no ingredients",
)
recipe = Recipe(
date_=data.date_,
source=data.source,
instructions=data.instructions,
garnishing=data.garnishing,
plating=data.plating,
notes=data.notes,
sku=recipe_sku,
recipe_yield=round(data.recipe_yield, 2),
)
for item in data.items:
product: Product = db.execute(select(Product).where(Product.id == item.product.id_)).scalar_one()
quantity = round(item.quantity, 2)
recipe.items.append(RecipeItem(product_id=product.id, quantity=quantity, description=item.description))
db.add(recipe)
for r_item in recipe.items:
r_item.recipe_id = recipe.id
db.add(r_item)
db.flush()
check_recursion(recipe_sku.product_id, set(), db)
db.commit()
return recipe_info(recipe)
@router.put("/{id_}", response_model=schemas.Recipe)
async def update_route(
id_: uuid.UUID,
data: schemas.RecipeIn,
request: Request,
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
db: SessionDep,
) -> schemas.Recipe:
try:
with SessionFuture() as db:
recipe: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
recipe: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
sku: StockKeepingUnit = db.execute(
select(StockKeepingUnit).where(StockKeepingUnit.id == data.sku.id_)
).scalar_one()
if sku.product_id in set(i.product.id_ for i in data.items):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot also be an ingredient",
)
recipe.sku = sku
recipe.recipe_yield = round(data.recipe_yield, 2)
recipe.date_ = data.date_
recipe.source = data.source
recipe.instructions = data.instructions
recipe.garnishing = data.garnishing
recipe.plating = data.plating
for i in range(len(recipe.items), 0, -1):
item = recipe.items[i - 1]
index = next((idx for (idx, d) in enumerate(data.items) if d.id_ == item.id), None)
product: Product
if index is not None:
new_item = data.items.pop(index)
product = db.execute(select(Product).where(Product.id == new_item.product.id_)).scalar_one()
item.product_id = new_item.product.id_
item.quantity = round(new_item.quantity, 2)
item.description = new_item.description
else:
db.delete(item)
recipe.items.remove(item)
db.flush()
for d_item in data.items:
product = db.execute(select(Product).where(Product.id == d_item.product.id_)).scalar_one()
quantity = round(d_item.quantity, 2)
recipe.items.append(
RecipeItem(product_id=product.id, quantity=quantity, description=d_item.description)
)
db.flush()
check_recursion(sku.product_id, set(), db)
db.commit()
return recipe_info(recipe)
except SQLAlchemyError as e:
sku: StockKeepingUnit = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == data.sku.id_)).scalar_one()
if sku.product_id in {i.product.id_ for i in data.items}:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot also be an ingredient",
)
recipe.sku = sku
recipe.recipe_yield = round(data.recipe_yield, 2)
recipe.date_ = data.date_
recipe.source = data.source
recipe.instructions = data.instructions
recipe.garnishing = data.garnishing
recipe.plating = data.plating
for i in range(len(recipe.items), 0, -1):
item = recipe.items[i - 1]
index = next((idx for (idx, d) in enumerate(data.items) if d.id_ == item.id), None)
product: Product
if index is not None:
new_item = data.items.pop(index)
product = db.execute(select(Product).where(Product.id == new_item.product.id_)).scalar_one()
item.product_id = new_item.product.id_
item.quantity = round(new_item.quantity, 2)
item.description = new_item.description
else:
db.delete(item)
recipe.items.remove(item)
db.flush()
for d_item in data.items:
product = db.execute(select(Product).where(Product.id == d_item.product.id_)).scalar_one()
quantity = round(d_item.quantity, 2)
recipe.items.append(RecipeItem(product_id=product.id, quantity=quantity, description=d_item.description))
db.flush()
check_recursion(sku.product_id, set(), db)
db.commit()
return recipe_info(recipe)
def check_recursion(product: uuid.UUID, visited: set[uuid.UUID], db: Session) -> None:
if product in visited:
@@ -176,114 +159,86 @@ def check_recursion(product: uuid.UUID, visited: set[uuid.UUID], db: Session) ->
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
request: Request,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
with SessionFuture() as db:
recipe_ids: Sequence[uuid.UUID] = (
db.execute(
select(func.distinct(RecipeItem.recipe_id)).where(
RecipeItem.product_id
== select(StockKeepingUnit.product_id)
.where(StockKeepingUnit.id == select(Recipe.sku_id).where(Recipe.id == id_).scalar_subquery())
.scalar_subquery(),
)
recipe_ids: Sequence[uuid.UUID] = (
db.execute(
select(func.distinct(RecipeItem.recipe_id)).where(
RecipeItem.product_id
== select(StockKeepingUnit.product_id)
.where(StockKeepingUnit.id == select(Recipe.sku_id).where(Recipe.id == id_).scalar_subquery())
.scalar_subquery(),
)
.scalars()
.all()
)
if len(recipe_ids) > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="This recipe has other recipes dependent on it and cannot be deleted",
)
.scalars()
.all()
)
if len(recipe_ids) > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="This recipe has other recipes dependent on it and cannot be deleted",
)
db.execute(delete(RecipeItem).where(RecipeItem.recipe_id == id_))
db.execute(delete(Recipe).where(Recipe.id == id_))
db.execute(delete(RecipeItem).where(RecipeItem.recipe_id == id_))
db.execute(delete(Recipe).where(Recipe.id == id_))
db.commit()
db.commit()
@router.get("", response_model=schemas.RecipeBlank)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
) -> schemas.RecipeBlank:
return recipe_blank()
@router.get("/list", response_model=list[schemas.Recipe])
async def show_list(
user: UserToken = Depends(get_user),
) -> list[schemas.Recipe]:
with SessionFuture() as db:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Recipe.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Recipe.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Recipe.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Recipe.date_,
),
)
list_: Sequence[Recipe] = (
db.execute(
select(Recipe)
.join(Recipe.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group)
.options(
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions),
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group),
)
.order_by(ProductVersion.name)
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.Recipe]:
list_: Sequence[Recipe] = (
db.execute(
select(Recipe)
.join(Recipe.sku)
.join(SkuVersion, onclause=_sv_onclause(Recipe.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(Recipe.date_))
.join(ProductVersion.product_group)
.options(
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions),
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group),
)
.unique()
.scalars()
.all()
.order_by(ProductVersion.name)
)
return [
schemas.Recipe(
id_=item.id,
sku=ProductLink(id_=item.sku.id, name=item.sku.product.versions[0].name),
date_=item.date_,
source=item.source,
instructions=item.instructions,
garnishing=item.garnishing,
plating=item.plating,
recipe_yield=item.recipe_yield,
notes=item.notes,
product_group_id=item.sku.product.versions[0].product_group_id,
units=item.sku.versions[0].units,
items=[],
)
for item in list_
]
.unique()
.scalars()
.all()
)
return [
schemas.Recipe(
id_=item.id,
sku=ProductLink(id_=item.sku.id, name=item.sku.product.versions[0].name),
date_=item.date_,
source=item.source,
instructions=item.instructions,
garnishing=item.garnishing,
plating=item.plating,
recipe_yield=item.recipe_yield,
notes=item.notes,
product_group_id=item.sku.product.versions[0].product_group_id,
units=item.sku.versions[0].units,
items=[],
)
for item in list_
]
# @router.get("/ingredient-details/{id_}", response_model=ProductSku)
# def show_ingredient_id(
# id_: uuid.UUID,
# user: UserToken = Security(get_user, scopes=["recipes"]),
# user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
# ) -> ProductSku:
# with SessionFuture() as db:
# product: Product = db.execute(select(Product).join(Product.skus).where(Product.id == id_)).unique().scalar_one()
@@ -297,108 +252,104 @@ async def show_list(
@router.get("/pdf/{id_}", response_class=FileResponse)
def show_pdf(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.Recipe:
with SessionFuture() as db:
# template = db.execute(select(RecipeTemplate.text).where(RecipeTemplate.selected == True)).scalar_one()
# with tempfile.TemporaryDirectory() as tmpdir:
# with open(tmpdir + "/recipe.tex", 'w') as rt:
# rt.write(template)
# subprocess.run(f"docker run -i --rm --name latex --volume \"{tmpdir}\":/data -w /data --user $(id -u):$(id -g) texlive/texlive:latest pdflatex recipe.tex")
item: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
return recipe_info(item)
# template = db.execute(select(RecipeTemplate.text).where(RecipeTemplate.selected == True)).scalar_one()
# with tempfile.TemporaryDirectory() as tmpdir:
# with open(tmpdir + "/recipe.tex", 'w') as rt:
# rt.write(template)
# subprocess.run(f"docker run -i --rm --name latex --volume \"{tmpdir}\":/data -w /data --user $(id -u):$(id -g) texlive/texlive:latest pdflatex recipe.tex")
item: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
return recipe_info(item)
@router.get("/xlsx", response_class=StreamingResponse)
def get_report(
p: uuid.UUID,
db: SessionDep,
pg: uuid.UUID | None = None,
) -> StreamingResponse:
with SessionFuture() as db:
calculate_prices(p, db)
db.commit()
calculate_prices(p, db)
db.commit()
prices: list[tuple[str, str, Decimal]] = []
with SessionFuture() as db:
pq = (
db.execute(select(Price).where(Price.period_id == p).options(joinedload(Price.product, innerjoin=True)))
.unique()
.scalars()
.all()
)
prices = [(i.product.versions[-1].name, i.product.versions[-1].fraction_units, i.price) for i in pq]
pq = (
db.execute(select(Price).where(Price.period_id == p).options(joinedload(Price.product, innerjoin=True)))
.unique()
.scalars()
.all()
)
prices = [(i.product.versions[-1].name, i.product.versions[-1].fraction_units, i.price) for i in pq]
list_: Sequence[Recipe] = []
print("test2")
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")
db.rollback()
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_,
),
)
# 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_,
),
)
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_,
),
)
item_product_version_onclause = and_(
ItemProductVersion.product_id == RecipeItem.product_id,
or_(
ItemProductVersion.valid_from == None, # noqa: E711
ItemProductVersion.valid_from <= Recipe.date_,
),
or_(
ItemProductVersion.valid_till == None, # noqa: E711
ItemProductVersion.valid_till >= Recipe.date_,
),
)
q = (
select(Recipe)
.join(Recipe.items)
.join(RecipeItem.product)
.join(ItemProductVersion, item_product_version_onclause)
.join(Recipe.sku)
.join(CurrentSkuVersion, sku_version_onclause)
.join(StockKeepingUnit.product)
.join(RecipeProductVersion, recipe_product_version_onclause)
.join(RecipeProductVersion.product_group)
.options(
# Eagerly load ingredients and their product & product_version
contains_eager(Recipe.items)
.contains_eager(RecipeItem.product)
.contains_eager(Product.versions, alias=ItemProductVersion),
# Eagerly load sku
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions, alias=RecipeProductVersion)
.contains_eager(RecipeProductVersion.product_group),
# SkuVersion for the SKU (if needed)
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions, alias=CurrentSkuVersion),
)
q = (
select(Recipe)
.join(Recipe.items)
.join(RecipeItem.product)
.join(ItemProductVersion, item_product_version_onclause)
.join(Recipe.sku)
.join(CurrentSkuVersion, sku_version_onclause)
.join(StockKeepingUnit.product)
.join(RecipeProductVersion, recipe_product_version_onclause)
.join(RecipeProductVersion.product_group)
.options(
# Eagerly load ingredients and their product & product_version
contains_eager(Recipe.items)
.contains_eager(RecipeItem.product)
.contains_eager(Product.versions, alias=ItemProductVersion),
# Eagerly load sku
contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions, alias=RecipeProductVersion)
.contains_eager(RecipeProductVersion.product_group),
# SkuVersion for the SKU (if needed)
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions, alias=CurrentSkuVersion),
)
if pg is not None:
q = q.where(RecipeProductVersion.product_group_id == pg)
list_ = db.execute(q).unique().scalars().all()
)
if pg is not None:
q = q.where(RecipeProductVersion.product_group_id == pg)
list_ = db.execute(q).unique().scalars().all()
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.versions[0].name))
e.seek(0)
@@ -420,7 +371,7 @@ def excel(prices: list[tuple[str, str, Decimal]], recipes: list[Recipe]) -> Byte
ws.cell(row=i, column=2, value=p[1])
ws.cell(row=i, column=3, value=p[2])
pgs = set([x.sku.product.versions[0].product_group.name for x in recipes])
pgs = {x.sku.product.versions[0].product_group.name for x in recipes}
for pg in pgs:
wb.create_sheet(pg)
rows: defaultdict[str, int] = defaultdict(lambda: 1)
@@ -428,7 +379,6 @@ def excel(prices: list[tuple[str, str, Decimal]], recipes: list[Recipe]) -> Byte
for recipe in recipes:
ws = wb[recipe.sku.product.versions[0].product_group.name]
row = rows[recipe.sku.product.versions[0].product_group.name]
print(row)
ings = len(recipe.items)
ing_from = row + 2
ing_till = ing_from + ings - 1
@@ -459,15 +409,13 @@ def excel(prices: list[tuple[str, str, Decimal]], recipes: list[Recipe]) -> Byte
@router.get("/nutrition", response_class=StreamingResponse)
def get_nutrition(
p: uuid.UUID | None = None,
db: SessionDep,
p: uuid.UUID | None = None, # noqa: ARG001
) -> StreamingResponse:
date_ = date.today()
with SessionFuture() as db:
calculate_nutrition(date_, db)
db.commit()
list_: list[NutritionalInformation] = []
with SessionFuture() as db:
list_ = report_nutrition(date_, db)
calculate_nutrition(date_, db)
db.commit()
list_ = report_nutrition(date_, db)
e = nut(sorted(list_, key=lambda r: r.name))
e.seek(0)
@@ -480,7 +428,7 @@ def nut(products: list[NutritionalInformation]) -> BytesIO:
ws = wb.active
assert ws is not None
ws.title = "Ingredients"
pgs = set([x.product_group for x in products])
pgs = {x.product_group for x in products}
pgs.remove("Ingredients")
for pg in pgs:
wb.create_sheet(pg)
@@ -560,12 +508,10 @@ def register_styles(wb: Workbook) -> tuple[NamedStyle, NamedStyle, NamedStyle, N
@router.get("/{id_}", response_model=schemas.Recipe)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.Recipe:
with SessionFuture() as db:
item: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
return recipe_info(item)
item: Recipe = db.execute(select(Recipe).where(Recipe.id == id_)).scalar_one()
return recipe_info(item)
def recipe_info(recipe: Recipe) -> schemas.Recipe:
+39 -64
View File
@@ -1,12 +1,13 @@
import datetime
import uuid
from fastapi import APIRouter, HTTPException, Security, status
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import delete, select, update
from sqlalchemy.exc import SQLAlchemyError
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.recipe_template import RecipeTemplate
from ..schemas import recipe_template as schemas
from ..schemas.user import UserToken
@@ -17,93 +18,67 @@ router = APIRouter()
@router.post("", response_model=None)
def save(
data: schemas.RecipeTemplateIn,
user: UserToken = Security(get_user, scopes=["recipes"]),
data: schemas.RecipeTemplateIn, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
try:
with SessionFuture() as db:
date_ = data.date_ if data.date_ is not None else datetime.date.today()
if data.selected:
db.execute(update(RecipeTemplate).values(selected=False))
item = RecipeTemplate(name=data.name, date_=date_, text=data.text, selected=data.selected)
db.add(item)
db.commit()
return None
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
date_ = data.date_ if data.date_ is not None else datetime.date.today()
if data.selected:
db.execute(update(RecipeTemplate).values(selected=False))
item = RecipeTemplate(name=data.name, date_=date_, text=data.text, selected=data.selected)
db.add(item)
db.commit()
return None
@router.put("/{id_}", response_model=None)
def update_route(
id_: uuid.UUID,
data: schemas.RecipeTemplateIn,
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
db: SessionDep,
) -> None:
try:
with SessionFuture() as db:
date_ = data.date_ if data.date_ is not None else datetime.date.today()
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
if data.selected and not item.selected:
db.execute(update(RecipeTemplate).where(RecipeTemplate.id != id_).values(selected=False))
item.name = data.name
item.text_ = data.text
item.selected = data.selected
item.date_ = date_
db.commit()
return None
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
date_ = data.date_ if data.date_ is not None else datetime.date.today()
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
if data.selected and not item.selected:
db.execute(update(RecipeTemplate).where(RecipeTemplate.id != id_).values(selected=False))
item.name = data.name
item.text_ = data.text
item.selected = data.selected
item.date_ = date_
db.commit()
return None
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
try:
with SessionFuture() as db:
db.execute(delete(RecipeTemplate).where(RecipeTemplate.id == id_))
db.commit()
return None
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
db.execute(delete(RecipeTemplate).where(RecipeTemplate.id == id_))
db.commit()
return None
@router.get("", response_model=schemas.RecipeTemplateIn)
def show_blank(
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
) -> schemas.RecipeTemplateIn:
return schemas.RecipeTemplateIn(name="", date_=datetime.date.today(), text="", selected=False)
@router.get("/list", response_model=list[schemas.RecipeTemplate])
async def show_list(
user: UserToken = Security(get_user, scopes=["recipes"]),
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> list[schemas.RecipeTemplate]:
with SessionFuture() as db:
list_ = db.execute(select(RecipeTemplate).order_by(RecipeTemplate.name)).scalars().all()
return [
schemas.RecipeTemplate(id_=i.id, name=i.name, date_=i.date_, text=i.text_, selected=i.selected)
for i in list_
]
list_ = db.execute(select(RecipeTemplate).order_by(RecipeTemplate.name)).scalars().all()
return [
schemas.RecipeTemplate(id_=i.id, name=i.name, date_=i.date_, text=i.text_, selected=i.selected) for i in list_
]
@router.get("/{id_}", response_model=schemas.RecipeTemplate)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["recipes"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.RecipeTemplate:
with SessionFuture() as db:
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
return schemas.RecipeTemplate(
id_=item.id, name=item.name, date_=item.date_, text=item.text_, selected=item.selected
)
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
return schemas.RecipeTemplate(
id_=item.id, name=item.name, date_=item.date_, text=item.text_, selected=item.selected
)
+67 -9
View File
@@ -1,17 +1,75 @@
from datetime import date, datetime
from datetime import date, datetime, timedelta
from fastapi import Request
def report_start_date(s: str | None = None) -> date | None:
return None if s is None else datetime.strptime(s, "%d-%b-%Y").date()
def report_start_date(request: Request, s: str | date | None = None) -> date:
session = request.session
if isinstance(s, date):
session["start"] = s.strftime("%d-%b-%Y")
return s
if s:
session["start"] = s
return datetime.strptime(s, "%d-%b-%Y").date()
if "start" not in session:
session["start"] = get_first_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["start"], "%d-%b-%Y").date()
def report_finish_date(f: str | None = None) -> date | None:
return None if f is None else datetime.strptime(f, "%d-%b-%Y").date()
def report_finish_date(request: Request, f: str | date | None = None) -> date:
session = request.session
if isinstance(f, date):
session["finish"] = f.strftime("%d-%b-%Y")
return f
if f:
session["finish"] = f
return datetime.strptime(f, "%d-%b-%Y").date()
if "finish" not in session:
session["finish"] = get_last_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["finish"], "%d-%b-%Y").date()
def report_s_date(s: str) -> date:
return datetime.strptime(s, "%d-%b-%Y").date()
def report_date(request: Request, d: str | date | None = None) -> date:
session = request.session
if isinstance(d, date):
session["date"] = d.strftime("%d-%b-%Y")
return d
if d:
session["date"] = d
return datetime.strptime(d, "%d-%b-%Y").date()
if "date" not in session:
session["date"] = get_last_day(date.today()).strftime("%d-%b-%Y")
return datetime.strptime(session["date"], "%d-%b-%Y").date()
def report_f_date(s: str) -> date:
return datetime.strptime(s, "%d-%b-%Y").date()
def get_first_day(dt: date, d_years: int = 0, d_months: int = 0) -> date:
# d_years, d_months are "deltas" to apply to dt
y, m = dt.year + d_years, dt.month + d_months
a, m = divmod(m - 1, 12)
return date(y + a, m + 1, 1)
def get_last_day(dt: date) -> date:
return get_first_day(dt, 0, 1) + timedelta(-1)
def op_report_start_date(request: Request, s: str | None = None) -> date | None:
session = request.session
if s:
session["start"] = s
return datetime.strptime(s, "%d-%b-%Y").date()
return None
def op_report_finish_date(request: Request, f: str | None = None) -> date | None:
session = request.session
if f:
session["finish"] = f
return datetime.strptime(f, "%d-%b-%Y").date()
return None
def path_date(request: Request, date_: str) -> date:
session = request.session
session["date"] = date_
return datetime.strptime(date_, "%d-%b-%Y").date()
@@ -1,14 +1,14 @@
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.account_type import AccountType
from ...models.journal import Journal
@@ -18,6 +18,7 @@ from ...routers.reports.closing_stock import get_closing_stock
from ...routers.reports.profit_loss import get_accumulated_profit
from ...schemas import balance_sheet as schemas
from ...schemas.user import UserToken
from . import path_date, report_finish_date
router = APIRouter()
@@ -25,22 +26,22 @@ router = APIRouter()
@router.get("", response_model=schemas.BalanceSheet)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["balance-sheet"]),
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["balance-sheet"])],
) -> schemas.BalanceSheet:
return schemas.BalanceSheet(date_=get_finish_date(request.session), body=[], footer=None)
return schemas.BalanceSheet(date_=finish_date, body=[], footer=None)
@router.get("/{date_}", response_model=schemas.BalanceSheet)
def report_data(
date_: str,
date_: Annotated[date, Depends(path_date)],
request: Request,
user: UserToken = Security(get_user, scopes=["balance-sheet"]),
user: Annotated[UserToken, Security(get_user, scopes=["balance-sheet"])],
db: SessionDep,
) -> schemas.BalanceSheet:
with SessionFuture() as db:
body, footer = build_balance_sheet(datetime.strptime(date_, "%d-%b-%Y"), db)
set_period(get_start_date(request.session), date_, request.session)
return schemas.BalanceSheet(date_=date_, body=body, footer=footer)
body, footer = build_balance_sheet(date_, db)
report_finish_date(request, date_)
return schemas.BalanceSheet(date_=date_, body=body, footer=footer)
def build_balance_sheet(date_: date, db: Session) -> tuple[list[schemas.BalanceSheetItem], schemas.BalanceSheetItem]:
+32 -38
View File
@@ -1,18 +1,19 @@
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.account_type import AccountType
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import report_finish_date, report_start_date
from ...schemas import cash_flow as schemas
from ...schemas.user import UserToken
@@ -22,12 +23,13 @@ router = APIRouter()
@router.get("", response_model=schemas.CashFlow)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["cash-flow"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["cash-flow"])],
) -> schemas.CashFlow:
return schemas.CashFlow(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
start_date=start_date,
finish_date=finish_date,
body=schemas.CashFlowBody(operating=[], investing=[], financing=[], details=[]),
footer=[],
)
@@ -35,43 +37,35 @@ def report_blank(
@router.get("/data", response_model=schemas.CashFlow)
def report_data(
request: Request,
s: str,
f: str,
user: UserToken = Security(get_user, scopes=["cash-flow"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["cash-flow"])],
db: SessionDep,
) -> schemas.CashFlow:
with SessionFuture() as db:
start_date = datetime.strptime(s, "%d-%b-%Y")
finish_date = datetime.strptime(f, "%d-%b-%Y")
body, footer = build_report(start_date, finish_date, db)
set_period(s, f, request.session)
return schemas.CashFlow(
start_date=s,
finish_date=f,
body=body,
footer=footer,
)
body, footer = build_report(start_date, finish_date, db)
return schemas.CashFlow(
start_date=start_date,
finish_date=finish_date,
body=body,
footer=footer,
)
@router.get("/{id_}", response_model=schemas.CashFlow)
def report_id(
id_: int,
request: Request,
s: str,
f: str,
user: UserToken = Security(get_user, scopes=["cash-flow"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["cash-flow"])],
db: SessionDep,
) -> schemas.CashFlow:
with SessionFuture() as db:
start_date = datetime.strptime(s, "%d-%b-%Y")
finish_date = datetime.strptime(f, "%d-%b-%Y")
body, footer = build_report_id(id_, start_date, finish_date, db)
set_period(s, f, request.session)
return schemas.CashFlow(
start_date=s,
finish_date=f,
body=body,
footer=footer,
)
body, footer = build_report_id(id_, start_date, finish_date, db)
return schemas.CashFlow(
start_date=start_date,
finish_date=finish_date,
body=body,
footer=footer,
)
def build_report(
+85 -150
View File
@@ -1,17 +1,15 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import and_
from sqlalchemy.exc import SQLAlchemyError
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import delete, distinct, func, or_, select, update
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.batch import Batch
from ...models.closing_stock import ClosingStock
@@ -26,12 +24,13 @@ from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.validations import check_journals_are_valid
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import path_date, report_finish_date
from ...schemas import closing_stock as schemas
from ...schemas.cost_centre import CostCentreLink
from ...schemas.product import ProductLink
from ...schemas.user import UserToken
from ...schemas.user_link import UserLink
from .. import get_lock_info
from .. import _pv_onclause, _sv_onclause, get_lock_info
router = APIRouter()
@@ -39,11 +38,11 @@ router = APIRouter()
@router.get("", response_model=schemas.ClosingStock)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["closing-stock"]),
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["closing-stock"])],
) -> schemas.ClosingStock:
return schemas.ClosingStock(
date_=get_finish_date(request.session), # type: ignore
date_=finish_date,
posted=False,
cost_centre=CostCentreLink(id_=CostCentre.cost_centre_purchase()),
items=[],
@@ -52,72 +51,53 @@ def report_blank(
@router.get("/{date_}", response_model=schemas.ClosingStock)
def report_data(
date_: str,
request: Request,
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["closing-stock"])],
db: SessionDep,
d: uuid.UUID | None = None,
user: UserToken = Security(get_user, scopes=["closing-stock"]),
) -> schemas.ClosingStock:
set_period(get_start_date(request.session), date_, request.session)
with SessionFuture() as db:
return full_report(datetime.strptime(date_, "%d-%b-%Y").date(), d or CostCentre.cost_centre_purchase(), db)
report_finish_date(request, date_)
return full_report(date_, d or CostCentre.cost_centre_purchase(), db)
@router.delete("/{date_}")
def delete_voucher(
date_: str,
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["closing-stock"])],
db: SessionDep,
d: uuid.UUID | None = None,
user: UserToken = Security(get_user, scopes=["closing-stock"]),
) -> None:
try:
cost_centre_id = d or CostCentre.cost_centre_purchase()
with SessionFuture() as db:
db.execute(
delete(ClosingStock)
.where(ClosingStock.date_ == date_, ClosingStock.cost_centre_id == cost_centre_id)
.execution_options(synchronize_session=False)
)
delete_old_vouchers(datetime.strptime(date_, "%d-%b-%Y").date(), cost_centre_id, db)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
cost_centre_id = d or CostCentre.cost_centre_purchase()
db.execute(
delete(ClosingStock)
.where(ClosingStock.date_ == date_, ClosingStock.cost_centre_id == cost_centre_id)
.execution_options(synchronize_session=False)
)
delete_old_vouchers(date_, cost_centre_id, db)
db.commit()
@router.post("/{date_}", response_model=schemas.ClosingStock)
def post_voucher(
date_: str,
date_: Annotated[date, Depends(path_date)],
d: uuid.UUID,
user: UserToken = Security(get_user, scopes=["post-vouchers"]),
user: Annotated[UserToken, Security(get_user, scopes=["post-vouchers"])],
db: SessionDep,
) -> schemas.ClosingStock:
try:
with SessionFuture() as db:
date_obj = datetime.strptime(date_, "%d-%b-%Y").date()
vouchers = (
db.execute(
select(Voucher.id).where(
Voucher.date_ == date_obj, Voucher.voucher_type == VoucherType.CLOSING_STOCK
)
)
.scalars()
.all()
)
vouchers = (
db.execute(
select(Journal.voucher_id).where(Journal.cost_centre_id == d, Journal.voucher_id.in_(vouchers))
)
.scalars()
.all()
)
db.execute(update(Voucher).where(Voucher.id.in_(vouchers)).values(posted=True, poster_id=user.id_))
db.commit()
return full_report(date_obj, d, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
vouchers = (
db.execute(select(Voucher.id).where(Voucher.date_ == date_, Voucher.voucher_type == VoucherType.CLOSING_STOCK))
.scalars()
.all()
)
vouchers = (
db.execute(select(Journal.voucher_id).where(Journal.cost_centre_id == d, Journal.voucher_id.in_(vouchers)))
.scalars()
.all()
)
db.execute(update(Voucher).where(Voucher.id.in_(vouchers)).values(posted=True, poster_id=user.id_))
db.commit()
return full_report(date_, d, db)
def full_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> schemas.ClosingStock:
@@ -143,29 +123,7 @@ def full_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> schemas.
def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> list[schemas.ClosingStockItem]:
amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount")
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[SkuVersion, Decimal, Decimal]] = (
db.execute(
select(SkuVersion, quantity_sum, amount_sum)
@@ -174,9 +132,9 @@ def build_report(date_: date, cost_centre_id: uuid.UUID, db: Session) -> list[sc
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.options(
contains_eager(SkuVersion.sku)
@@ -264,62 +222,51 @@ def get_closing_stock(date_: date, db: Session) -> Decimal:
@router.post("", response_model=schemas.ClosingStock)
def save_route(
request: Request,
data: schemas.ClosingStock,
user: UserToken = Security(get_user, scopes=["closing-stock"]),
user: Annotated[UserToken, Security(get_user, scopes=["closing-stock"])],
db: SessionDep,
) -> schemas.ClosingStock:
try:
departments = set(i.cost_centre.id_ for i in data.items if i.cost_centre is not None)
if data.cost_centre.id_ in departments:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Issue department cannot be same as main department",
)
if CostCentre.cost_centre_purchase() in departments:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products cannot be issued to purchase",
)
if CostCentre.cost_centre_overall() in departments:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products cannot be issued to overall",
)
if len([i for i in data.items if i.physical > i.quantity]) > 0:
x = [i for i in data.items if i.physical > i.quantity]
for y in x:
print(y)
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Physical Quantity cannot be more than computed quality.",
)
if len([i for i in data.items if i.physical != i.quantity and i.cost_centre is None]) > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products with change in quantity need a department",
)
with SessionFuture() as db:
count_ = db.execute(select(func.count()).where(ClosingStock.date_ == data.date_)).scalar_one()
if count_ == 0:
save_cs(data, db)
else:
update_cs(data.date_, data.cost_centre.id_, data.items[:], db)
delete_old_vouchers(data.date_, data.cost_centre.id_, db)
for dep in departments:
items = [d for d in data.items if d.cost_centre is not None and d.cost_centre.id_ == dep]
item = save(data.date_, items, user, db)
save_inventories(item, items, db)
amount: Decimal = sum(i.amount for i in item.inventories) or Decimal(0)
save_journals(item, data.cost_centre.id_, dep, amount, db)
check_journals_are_valid(item)
db.commit()
set_date(data.date_.strftime("%d-%b-%Y"), request.session)
return full_report(data.date_, data.cost_centre.id_, db)
except SQLAlchemyError as e:
departments = {i.cost_centre.id_ for i in data.items if i.cost_centre is not None}
if data.cost_centre.id_ in departments:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Issue department cannot be same as main department",
)
if CostCentre.cost_centre_purchase() in departments:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products cannot be issued to purchase",
)
if CostCentre.cost_centre_overall() in departments:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products cannot be issued to overall",
)
if len([i for i in data.items if i.physical > i.quantity]) > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Physical Quantity cannot be more than computed quality.",
)
if len([i for i in data.items if i.physical != i.quantity and i.cost_centre is None]) > 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Products with change in quantity need a department",
)
count_ = db.execute(select(func.count()).where(ClosingStock.date_ == data.date_)).scalar_one()
if count_ == 0:
save_cs(data, db)
else:
update_cs(data.date_, data.cost_centre.id_, data.items[:], db)
delete_old_vouchers(data.date_, data.cost_centre.id_, db)
for dep in departments:
items = [d for d in data.items if d.cost_centre is not None and d.cost_centre.id_ == dep]
item = save(data.date_, items, user, db)
save_inventories(item, items, db)
amount: Decimal = sum(i.amount for i in item.inventories) or Decimal(0)
save_journals(item, data.cost_centre.id_, dep, amount, db)
check_journals_are_valid(item)
db.commit()
return full_report(data.date_, data.cost_centre.id_, db)
def save_cs(data: schemas.ClosingStock, db: Session) -> None:
@@ -331,22 +278,10 @@ def save_cs(data: schemas.ClosingStock, db: Session) -> None:
def save(date_: date, items: list[schemas.ClosingStockItem], user: UserToken, db: Session) -> Voucher:
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
product_accounts = (
db.execute(
select(ProductVersion.account_id)
.join(Product, onclause=product_version_onclause)
.join(Product, onclause=_pv_onclause(date_))
.join(Product.skus)
.where(StockKeepingUnit.id.in_([i.product.id_ for i in items]))
)
+17 -23
View File
@@ -1,18 +1,19 @@
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import not_, select
from sqlalchemy.orm import Session
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import daybook as schemas
from ...schemas.user import UserToken
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -21,28 +22,21 @@ router = APIRouter()
@router.get("", response_model=schemas.Daybook)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["daybook"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["daybook"])],
db: SessionDep,
) -> schemas.Daybook:
return schemas.Daybook(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
)
@router.get("/{start}/{finish}", response_model=schemas.Daybook)
def report_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["daybook"]),
) -> schemas.Daybook:
start_date = datetime.strptime(start, "%d-%b-%Y")
finish_date = datetime.strptime(finish, "%d-%b-%Y")
with SessionFuture() as db:
if start_date is not None and finish_date is not None:
body = build_report(start_date, finish_date, db)
set_period(start, finish, request.session)
return schemas.Daybook(start_date=start_date, finish_date=finish_date, body=body)
else:
body = []
return schemas.Daybook(
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=body,
)
def build_report(start_date: date, finish_date: date, db: Session) -> list[schemas.DaybookItem]:
+36 -36
View File
@@ -1,6 +1,7 @@
from collections.abc import Sequence
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, Security
from sqlalchemy import desc, not_, or_, select
@@ -8,7 +9,7 @@ from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.functions import count
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
@@ -78,46 +79,45 @@ def build_report(
sq = sq.limit(page_size)
if page_index:
sq = sq.offset(page_size * page_index)
print("Counts:", counts, "\n\n\n\n", "Query:", query)
return db.execute(counts).scalar_one(), db.execute(query.where(Voucher.id.in_(sq))).unique().scalars().all()
@router.get("", response_model=schemas.Report)
def report_data(
start_date: date | None = Depends(report_start_date),
finish_date: date | None = Depends(report_finish_date),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["ledger"])],
db: SessionDep,
p: bool | None = None, # Posted
ps: int | None = 50, # Page Size
pi: int | None = 0, # Page Index
a: str | None = "date", # Active Sort
d: str | None = "desc", # Sort Direction
i: bool | None = False, # Show issue vouchers
user: UserToken = Security(get_user, scopes=["ledger"]),
ps: int = 50, # Page Size
pi: int = 0, # Page Index
a: str = "date", # Active Sort
d: str = "desc", # Sort Direction
i: bool = False, # Show issue vouchers
) -> schemas.Report:
assert i is not None and ps is not None and pi is not None and a is not None and d is not None
with SessionFuture() as db:
counts, report = build_report(start_date, finish_date, p, i, ps, pi, a, d, db)
return schemas.Report(
counts=counts,
report=[
schemas.Entries(
id_=voucher.id,
date_=voucher.date_,
url=[
"/",
voucher.voucher_type.name.replace("_", "-").lower(),
str(voucher.id),
],
type_=voucher.voucher_type.name.replace("_", " ").title(),
posted=voucher.posted,
narration=voucher.narration,
debit_names=[x.account.name for x in voucher.journals if x.debit == 1],
credit_names=[x.account.name for x in voucher.journals if x.debit != 1],
amount=sum(x.amount for x in voucher.journals if x.debit != 1) or Decimal(0),
creation_date=voucher.creation_date,
last_edit_date=voucher.last_edit_date,
user=UserLink(id_=voucher.user.id, name=voucher.user.name),
)
for voucher in report
],
)
counts, report = build_report(start_date, finish_date, p, i, ps, pi, a, d, db)
return schemas.Report(
counts=counts,
report=[
schemas.Entries(
id_=voucher.id,
date_=voucher.date_,
url=[
"/",
voucher.voucher_type.name.replace("_", "-").lower(),
str(voucher.id),
],
type_=voucher.voucher_type.name.replace("_", " ").title(),
posted=voucher.posted,
narration=voucher.narration,
debit_names=[x.account.name for x in voucher.journals if x.debit == 1],
credit_names=[x.account.name for x in voucher.journals if x.debit != 1],
amount=sum(x.amount for x in voucher.journals if x.debit != 1) or Decimal(0),
creation_date=voucher.creation_date,
last_edit_date=voucher.last_edit_date,
user=UserLink(id_=voucher.user.id, name=voucher.user.name),
)
for voucher in report
],
)
+27 -29
View File
@@ -1,16 +1,16 @@
import datetime
import uuid
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.journal import Journal
from ...models.voucher import Voucher
@@ -18,6 +18,7 @@ from ...models.voucher_type import VoucherType
from ...schemas import ledger as schemas
from ...schemas.account import AccountLink
from ...schemas.user import UserToken
from . import report_finish_date, report_start_date
router = APIRouter()
@@ -25,12 +26,13 @@ router = APIRouter()
@router.get("", response_model=schemas.Ledger)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["ledger"])],
) -> schemas.Ledger:
return schemas.Ledger(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
start_date=start_date,
finish_date=finish_date,
account=None,
body=[],
)
@@ -39,26 +41,22 @@ def show_blank(
@router.get("/{id_}", response_model=schemas.Ledger)
def show_data(
id_: uuid.UUID,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["ledger"])],
db: SessionDep,
) -> schemas.Ledger:
with SessionFuture() as db:
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
start_date = s if s is not None else get_start_date(request.session)
finish_date = f if f is not None else get_finish_date(request.session)
body = build_report(account.id, start_date, finish_date, db)
set_period(start_date, finish_date, request.session)
return schemas.Ledger(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
body = build_report(account.id, start_date, finish_date, db)
return schemas.Ledger(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: Session) -> list[schemas.LedgerItem]:
def build_report(account_id: uuid.UUID, start_date: date, finish_date: date, db: Session) -> list[schemas.LedgerItem]:
body = []
opening = opening_balance(account_id, start_date, db)
body.append(opening)
@@ -70,8 +68,8 @@ def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: S
.join(Journal.account)
.where(
Voucher.journals.any(Journal.account_id == account_id),
Voucher.date_ >= datetime.datetime.strptime(start_date, "%d-%b-%Y"),
Voucher.date_ <= datetime.datetime.strptime(finish_date, "%d-%b-%Y"),
Voucher.date_ >= start_date,
Voucher.date_ <= finish_date,
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
)
.order_by(Voucher.date_, Voucher.last_edit_date)
@@ -120,12 +118,12 @@ def build_report(account_id: uuid.UUID, start_date: str, finish_date: str, db: S
return body
def opening_balance(account_id: uuid.UUID, start_date: str, db: Session) -> schemas.LedgerItem:
def opening_balance(account_id: uuid.UUID, start_date: date, db: Session) -> schemas.LedgerItem:
opening = db.execute(
select(func.sum(Journal.amount * Journal.debit))
.join(Journal.voucher)
.where(
Voucher.date_ < datetime.datetime.strptime(start_date, "%d-%b-%Y"),
Voucher.date_ < start_date,
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
Journal.account_id == account_id,
)
@@ -2,24 +2,24 @@ import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, HTTPException, Request, Security, status
from sqlalchemy import and_, desc, func, or_
from sqlalchemy.exc import SQLAlchemyError
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from sqlalchemy import desc, func, or_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_date, set_date
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.mozimo_stock_register import MozimoStockRegister
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas import mozimo_daily_register as schemas
from ...schemas.product import ProductLink
from ...schemas.user import UserToken
from .. import _pv_onclause, _sv_onclause
from . import path_date, report_date
router = APIRouter()
@@ -27,59 +27,37 @@ router = APIRouter()
@router.get("", response_model=schemas.MozimoDailyRegister)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
date_: Annotated[date, Depends(report_date)],
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
) -> schemas.MozimoDailyRegister:
return schemas.MozimoDailyRegister(date_=get_date(request.session), body=[])
return schemas.MozimoDailyRegister(date_=date_, body=[])
@router.get("/{date_}", response_model=schemas.MozimoDailyRegister)
def show_data(
date_: str,
date_: Annotated[date, Depends(path_date)],
request: Request,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
db: SessionDep,
) -> schemas.MozimoDailyRegister:
with SessionFuture() as db:
d = datetime.strptime(date_, "%d-%b-%Y").date()
body = build_report(d, db)
set_date(date_, request.session)
return schemas.MozimoDailyRegister(
date_=d,
body=body,
)
body = build_report(date_, db)
report_date(request, date_)
return schemas.MozimoDailyRegister(
date_=date_,
body=body,
)
def build_report(date_: date, db: Session) -> list[schemas.MozimoDailyRegisterItem]:
body = []
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= date_,
),
)
products = (
db.execute(
select(ProductVersion.name, SkuVersion.units, SkuVersion.sku_id)
.select_from(Product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(Product.skus)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(date_))
.where(ProductVersion.product_group_id == uuid.UUID("dad46805-f577-4e5b-8073-9b788e0173fc")) # Menu items
.order_by(ProductVersion.name, SkuVersion.units)
)
@@ -151,100 +129,88 @@ def opening_balance(product_id: uuid.UUID, start_date: date, db: Session) -> Dec
@router.post("/{date_}", response_model=schemas.MozimoDailyRegister)
def save_route(
date_: str,
request: Request,
date_: Annotated[date, Depends(path_date)],
data: schemas.MozimoDailyRegister,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
db: SessionDep,
) -> schemas.MozimoDailyRegister:
d = datetime.strptime(date_, "%d-%b-%Y").date()
try:
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
for item in data.body:
ob = opening_balance(item.product.id_, d, db)
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
else:
closing = ob + item.received - item.sale - item.nc
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
is_blank = (
item.received == 0
and item.sale == 0
and item.nc == 0
and item.display is None
and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == item.product.id_,
MozimoStockRegister.date_ == d,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
elif not is_blank:
entry = MozimoStockRegister(
d, item.received, item.sale, item.nc, item.display, item.ageing, item.product.id_
)
db.add(entry)
body = build_report(d, db)
db.commit()
return schemas.MozimoDailyRegister(
date_=d,
body=body,
)
except SQLAlchemyError as e:
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
now = datetime.now(UTC).replace(tzinfo=None)
for item in data.body:
ob = opening_balance(item.product.id_, date_, db)
if item.ageing is not None or item.display is not None:
closing = (item.ageing or 0) + (item.display or 0)
else:
closing = ob + item.received - item.sale - item.nc
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
is_blank = (
item.received == 0 and item.sale == 0 and item.nc == 0 and item.display is None and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == item.product.id_,
MozimoStockRegister.date_ == date_,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
elif not is_blank:
entry = MozimoStockRegister(
date_, item.received, item.sale, item.nc, item.display, item.ageing, item.product.id_
)
db.add(entry)
body = build_report(date_, db)
db.commit()
return schemas.MozimoDailyRegister(
date_=date_,
body=body,
)
@@ -2,15 +2,14 @@ import uuid
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from sqlalchemy.exc import SQLAlchemyError
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
from ...core.security import get_current_active_user as get_user
from ...core.session import set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.mozimo_stock_register import MozimoStockRegister
from ...models.stock_keeping_unit import StockKeepingUnit
from ...schemas import mozimo_product_register as schemas
@@ -26,9 +25,9 @@ router = APIRouter()
@router.get("", response_model=schemas.MozimoProductRegister)
def show_blank(
start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date),
user: UserToken = Security(get_user, scopes=["product-ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
) -> schemas.MozimoProductRegister:
return schemas.MozimoProductRegister(
start_date=start_date,
@@ -41,21 +40,19 @@ def show_blank(
@router.get("/{id_}", response_model=schemas.MozimoProductRegister)
def show_data(
id_: uuid.UUID,
request: Request,
start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date),
user: UserToken = Security(get_user, scopes=["product-ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
db: SessionDep,
) -> schemas.MozimoProductRegister:
with SessionFuture() as db:
sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == id_)).scalar_one()
body = build_report(sku.id, start_date, finish_date, db)
set_period(start_date, finish_date, request.session)
return schemas.MozimoProductRegister(
start_date=start_date,
finish_date=finish_date,
product=ProductLink(id_=sku.id, name=f"{sku.product.versions[-1].name} ({sku.versions[-1].units})"),
body=body,
)
sku = db.execute(select(StockKeepingUnit).where(StockKeepingUnit.id == id_)).scalar_one()
body = build_report(sku.id, start_date, finish_date, db)
return schemas.MozimoProductRegister(
start_date=start_date,
finish_date=finish_date,
product=ProductLink(id_=sku.id, name=f"{sku.product.versions[-1].name} ({sku.versions[-1].units})"),
body=body,
)
def build_report(
@@ -100,106 +97,95 @@ def build_report(
@router.post("", response_model=schemas.MozimoProductRegister)
def save_route(
request: Request,
data: schemas.MozimoProductRegister,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
db: SessionDep,
) -> schemas.MozimoProductRegister:
try:
if not data.product or not data.product.id_:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot be null",
)
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
with SessionFuture() as db:
now = datetime.now(UTC).replace(tzinfo=None)
ob = opening_balance(data.product.id_, data.start_date, db)
for item in data.body:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or Decimal(0)) + (item.display or Decimal(0))
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
is_blank = (
item.received == 0
and item.sale == 0
and item.nc == 0
and item.display is None
and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == data.product.id_,
MozimoStockRegister.date_ == item.date_,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
elif not is_blank:
entry = MozimoStockRegister(
item.date_, item.received, item.sale, item.nc, item.display, item.ageing, data.product.id_
)
db.add(entry)
body = build_report(data.product.id_, data.start_date, data.finish_date, db)
db.commit()
return schemas.MozimoProductRegister(
start_date=data.start_date,
finish_date=data.finish_date,
product=data.product,
body=body,
)
except SQLAlchemyError as e:
if not data.product or not data.product.id_:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Product cannot be null",
)
if any(i for i in data.body if i.received < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Received cannot be less than 0.",
)
if any(i for i in data.body if i.sale < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Sale cannot be less than 0.",
)
if any(i for i in data.body if i.nc < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="No Charge cannot be less than 0.",
)
if any(i for i in data.body if i.display is not None and i.display < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity on display can be Blank, but cannot be less than 0.",
)
if any(i for i in data.body if i.ageing is not None and i.ageing < 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Quantity in ageing room can be Blank, but cannot be less than 0.",
)
now = datetime.now(UTC).replace(tzinfo=None)
ob = opening_balance(data.product.id_, data.start_date, db)
for item in data.body:
if item.ageing is not None or item.display is not None:
closing = (item.ageing or Decimal(0)) + (item.display or Decimal(0))
else:
closing = ob + item.received - item.sale - item.nc
ob = closing # Setting the cb as ob for next iteration
if closing < 0:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Closing Stock cannot be less than 0.",
)
for item in data.body:
is_blank = (
item.received == 0 and item.sale == 0 and item.nc == 0 and item.display is None and item.ageing is None
)
old = (
db.execute(
select(MozimoStockRegister).where(
MozimoStockRegister.sku_id == data.product.id_,
MozimoStockRegister.date_ == item.date_,
)
)
.scalars()
.one_or_none()
)
if old is not None:
if is_blank:
db.delete(old)
elif (
old.received != item.received
or old.sale != item.sale
or old.nc != item.nc
or old.display != item.display
or old.ageing != item.ageing
):
old.received = item.received
old.sale = item.sale
old.nc = item.nc
old.display = item.display
old.ageing = item.ageing
old.last_edit_date = now
elif not is_blank:
entry = MozimoStockRegister(
item.date_, item.received, item.sale, item.nc, item.display, item.ageing, data.product.id_
)
db.add(entry)
body = build_report(data.product.id_, data.start_date, data.finish_date, db)
db.commit()
return schemas.MozimoProductRegister(
start_date=data.start_date,
finish_date=data.finish_date,
product=data.product,
body=body,
)
@@ -12,7 +12,7 @@
# from sqlalchemy.orm import Session
# from ...core.security import get_user
# from ...db.session import SessionFuture
# from ...db.session import SessionDep
# from ...models.employee import Employee
# from ...models.fingerprint import Fingerprint
# from ...schemas import fingerprint as schemas
@@ -1,19 +1,20 @@
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import net_transactions as schemas
from ...schemas.user import UserToken
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -22,32 +23,20 @@ router = APIRouter()
@router.get("", response_model=schemas.NetTransactions)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["net-transactions"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["net-transactions"])],
db: SessionDep,
) -> schemas.NetTransactions:
return schemas.NetTransactions(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
)
@router.get("/{start}/{finish}", response_model=schemas.NetTransactions)
def show_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["net-transactions"]),
) -> schemas.NetTransactions:
set_period(start, finish, request.session)
with SessionFuture() as db:
if start_date is not None and finish_date is not None:
body = build_report(start_date, finish_date, db)
return schemas.NetTransactions(start_date=start_date, finish_date=finish_date, body=body)
else:
body = []
return schemas.NetTransactions(
start_date=start,
finish_date=finish,
body=build_report(
datetime.strptime(start, "%d-%b-%Y"),
datetime.strptime(finish, "%d-%b-%Y"),
db,
),
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=body,
)
@@ -1,13 +1,14 @@
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import and_, or_, select
from sqlalchemy import select
from sqlalchemy.orm import Session, contains_eager
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.batch import Batch
from ...models.inventory import Inventory
from ...models.journal import Journal
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.rate_contract import RateContract
from ...models.sku_version import SkuVersion
@@ -16,6 +17,7 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import non_contract_purchase as schemas
from ...schemas.user import UserToken
from .. import _pv_onclause, _sv_onclause
router = APIRouter()
@@ -23,10 +25,9 @@ router = APIRouter()
@router.post("", response_model=list[schemas.NonContractPurchase])
def non_contract_purchases(
user: UserToken = Security(get_user, scopes=["product-ledger"]),
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])], db: SessionDep
) -> list[schemas.NonContractPurchase]:
with SessionFuture() as db:
return report(db)
return report(db)
def report(db: Session) -> list[schemas.NonContractPurchase]:
@@ -45,29 +46,6 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
.all()
)
list_: list[schemas.NonContractPurchase] = []
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
for rc in rcs:
for item in rc.items:
invs = db.execute(
@@ -77,9 +55,9 @@ def report(db: Session) -> list[schemas.NonContractPurchase]:
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.where(
Batch.sku_id == item.sku_id,
Voucher.date_ >= rc.valid_from,
@@ -1,16 +1,15 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, or_
from fastapi import APIRouter, Depends, Security
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.batch import Batch
from ...models.cost_centre import CostCentre
from ...models.inventory import Inventory
@@ -20,9 +19,11 @@ from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import report_finish_date, report_start_date
from ...schemas import product_ledger as schemas
from ...schemas.product import ProductLink
from ...schemas.user import UserToken
from .. import _sv_onclause
router = APIRouter()
@@ -30,12 +31,13 @@ router = APIRouter()
@router.get("", response_model=schemas.ProductLedger)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
) -> schemas.ProductLedger:
return schemas.ProductLedger(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
start_date=start_date,
finish_date=finish_date,
product=None,
body=[],
)
@@ -44,28 +46,24 @@ def show_blank(
@router.get("/{id_}", response_model=schemas.ProductLedger)
def show_data(
id_: uuid.UUID,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["product-ledger"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["product-ledger"])],
db: SessionDep,
) -> schemas.ProductLedger:
with SessionFuture() as db:
product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
start_date = s if s is not None else get_start_date(request.session)
finish_date = f if f is not None else get_finish_date(request.session)
body = build_report(
product.id,
datetime.strptime(start_date, "%d-%b-%Y"),
datetime.strptime(finish_date, "%d-%b-%Y"),
db,
)
set_period(start_date, finish_date, request.session)
return schemas.ProductLedger(
start_date=start_date,
finish_date=finish_date,
product=ProductLink(id_=product.id, name=product.versions[-1].name),
body=body,
)
product = db.execute(select(Product).where(Product.id == id_)).scalar_one()
body = build_report(
product.id,
start_date,
finish_date,
db,
)
return schemas.ProductLedger(
start_date=start_date,
finish_date=finish_date,
product=ProductLink(id_=product.id, name=product.versions[-1].name),
body=body,
)
def build_report(
@@ -74,17 +72,6 @@ def build_report(
running_total_q, running_total_a, opening = opening_balance(product_id, start_date, db)
body = opening
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[Voucher, Inventory, Journal, SkuVersion]] = (
db.execute( # type: ignore[assignment]
select(Voucher, Inventory, Journal, SkuVersion)
@@ -95,7 +82,7 @@ def build_report(
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.options(
contains_eager(Voucher.journals).contains_eager(Journal.account),
@@ -162,18 +149,6 @@ def build_report(
def opening_balance(
product_id: uuid.UUID, start_date: date, db: Session
) -> tuple[Decimal, Decimal, list[schemas.ProductLedgerItem]]:
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
row = db.execute(
select(
SkuVersion.units,
@@ -185,7 +160,7 @@ def opening_balance(
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.where(
Voucher.id == Inventory.voucher_id,
Voucher.id == Journal.voucher_id,
+22 -27
View File
@@ -1,14 +1,14 @@
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.account_type import AccountType
from ...models.journal import Journal
@@ -17,6 +17,7 @@ from ...models.voucher_type import VoucherType
from ...routers.reports.closing_stock import get_closing_stock, get_opening_stock
from ...schemas import profit_loss as schemas
from ...schemas.user import UserToken
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -25,31 +26,25 @@ router = APIRouter()
@router.get("", response_model=schemas.ProfitLoss)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["profit-&-loss"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["profit-&-loss"])],
db: SessionDep,
) -> schemas.ProfitLoss:
return schemas.ProfitLoss(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
footer=schemas.ProfitLossItem(group="", total=Decimal(0), order=0),
)
@router.get("/{start}/{finish}", response_model=schemas.ProfitLoss)
def report_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["profit-&-loss"]),
) -> schemas.ProfitLoss:
with SessionFuture() as db:
body, footer = build_profit_loss(
datetime.strptime(start, "%d-%b-%Y"), datetime.strptime(finish, "%d-%b-%Y"), db
)
set_period(start, finish, request.session)
if start_date is not None and finish_date is not None:
body, footer = build_profit_loss(start_date, finish_date, db)
return schemas.ProfitLoss(
start_date=start,
finish_date=finish,
start_date=start_date,
finish_date=finish_date,
body=body,
footer=footer,
)
else:
body = []
footer = schemas.ProfitLossItem(group="", total=Decimal(0), order=0)
return schemas.ProfitLoss(
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=body,
footer=footer,
)
@@ -1,12 +1,12 @@
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, or_, select
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import select
from sqlalchemy.orm import Session, contains_eager
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.batch import Batch
from ...models.inventory import Inventory
from ...models.journal import Journal
@@ -17,6 +17,8 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import purchase_entries as schemas
from ...schemas.user import UserToken
from .. import _pv_onclause
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -25,43 +27,30 @@ router = APIRouter()
@router.get("", response_model=schemas.PurchaseEntries)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["purchase-entries"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["purchase-entries"])],
db: SessionDep,
) -> schemas.PurchaseEntries:
return schemas.PurchaseEntries(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
)
@router.get("/{start}/{finish}", response_model=schemas.PurchaseEntries)
def report_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["purchase-entries"]),
) -> schemas.PurchaseEntries:
start_date = datetime.strptime(start, "%d-%b-%Y")
finish_date = datetime.strptime(finish, "%d-%b-%Y")
with SessionFuture() as db:
if start_date is not None and finish_date is not None:
body = build_report(start_date, finish_date, db)
set_period(start, finish, request.session)
return schemas.PurchaseEntries(start_date=start, finish_date=finish, body=body)
return schemas.PurchaseEntries(
start_date=start_date,
finish_date=start_date,
body=body,
)
else:
body = []
return schemas.PurchaseEntries(
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=body,
)
def build_report(start_date: date, finish_date: date, db: Session) -> list[schemas.PurchaseEntriesItem]:
body = []
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
query: list[Voucher] = list(
db.execute(
select(Voucher)
@@ -71,7 +60,7 @@ def build_report(start_date: date, finish_date: date, db: Session) -> list[schem
.join(Inventory.batch)
.join(Batch.sku)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.options(
contains_eager(Voucher.journals).contains_eager(Journal.account),
contains_eager(Voucher.inventories)
+29 -54
View File
@@ -1,19 +1,18 @@
from datetime import datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, not_, or_
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import desc, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.batch import Batch
from ...models.cost_centre import CostCentre
from ...models.inventory import Inventory
from ...models.journal import Journal
from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
@@ -21,6 +20,8 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import purchases as schemas
from ...schemas.user import UserToken
from .. import _pv_onclause, _sv_onclause
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -29,62 +30,36 @@ router = APIRouter()
@router.get("", response_model=schemas.Purchases)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["purchases"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["purchases"])],
db: SessionDep,
) -> schemas.Purchases:
return schemas.Purchases(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
footer=schemas.PurchasesItem(name="Total", quantity=Decimal(0), rate=Decimal(0), url=[], amount=Decimal(0)),
)
@router.get("/{start}/{finish}", response_model=schemas.Purchases)
def report_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["purchases"]),
) -> schemas.Purchases:
with SessionFuture() as db:
body, footer = build_report(start, finish, db)
set_period(start, finish, request.session)
if start_date is not None and finish_date is not None:
body, footer = build_report(start_date, finish_date, db)
return schemas.Purchases(
start_date=start,
finish_date=finish,
start_date=start_date,
finish_date=finish_date,
body=body,
footer=footer,
)
else:
body = []
footer = schemas.PurchasesItem(name="Total", quantity=Decimal(0), rate=Decimal(0), url=[], amount=Decimal(0))
return schemas.Purchases(
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=body,
footer=footer,
)
def build_report(
start_date: str, finish_date: str, db: Session
start_date: date, finish_date: date, db: Session
) -> tuple[list[schemas.PurchasesItem], schemas.PurchasesItem]:
body = []
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
amount_sum = func.sum(Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax)).label("amount")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[ProductVersion, Decimal, Decimal]] = db.execute(
select(ProductVersion, quantity_sum, amount_sum)
.select_from(Voucher)
@@ -92,13 +67,13 @@ def build_report(
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= datetime.strptime(start_date, "%d-%b-%Y"),
Voucher.date_ <= datetime.strptime(finish_date, "%d-%b-%Y"),
Voucher.date_ >= start_date,
Voucher.date_ <= finish_date,
not_(Voucher.voucher_type.in_([VoucherType.ISSUE, VoucherType.CLOSING_STOCK])),
Journal.cost_centre_id == CostCentre.cost_centre_purchase(),
)
@@ -1,16 +1,15 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, or_
from fastapi import APIRouter, Depends, Security
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import case, func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.batch import Batch
from ...models.cost_centre import CostCentre
@@ -23,8 +22,10 @@ from ...models.sku_version import SkuVersion
from ...models.stock_keeping_unit import StockKeepingUnit
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import report_finish_date, report_start_date
from ...schemas import raw_material_cost as schemas
from ...schemas.user import UserToken
from .. import _pv_onclause, _sv_onclause
router = APIRouter()
@@ -32,12 +33,13 @@ router = APIRouter()
@router.get("", response_model=schemas.RawMaterialCost)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["raw-material-cost"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["raw-material-cost"])],
) -> schemas.RawMaterialCost:
return schemas.RawMaterialCost(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
start_date=start_date,
finish_date=finish_date,
body=[],
footer=None,
)
@@ -45,43 +47,35 @@ def report_blank(
@router.get("/data", response_model=schemas.RawMaterialCost)
def report_data(
request: Request,
s: str,
f: str,
user: UserToken = Security(get_user, scopes=["raw-material-cost"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["raw-material-cost"])],
db: SessionDep,
) -> schemas.RawMaterialCost:
with SessionFuture() as db:
body, footer = build_report(
datetime.strptime(s, "%d-%b-%Y").date(), datetime.strptime(f, "%d-%b-%Y").date(), db
)
set_period(s, f, request.session)
return schemas.RawMaterialCost(
start_date=s,
finish_date=f,
body=body,
footer=footer,
)
body, footer = build_report(start_date, finish_date, db)
return schemas.RawMaterialCost(
start_date=start_date,
finish_date=finish_date,
body=body,
footer=footer,
)
@router.get("/{id_}", response_model=schemas.RawMaterialCost)
def report_id(
id_: uuid.UUID,
request: Request,
s: str,
f: str,
user: UserToken = Security(get_user, scopes=["raw-material-cost"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["raw-material-cost"])],
db: SessionDep,
) -> schemas.RawMaterialCost:
with SessionFuture() as db:
body = build_report_id(
id_, datetime.strptime(s, "%d-%b-%Y").date(), datetime.strptime(f, "%d-%b-%Y").date(), db
)
set_period(s, f, request.session)
return schemas.RawMaterialCost(
id_=id_,
start_date=s,
finish_date=f,
body=body,
)
body = build_report_id(id_, start_date, finish_date, db)
return schemas.RawMaterialCost(
id_=id_,
start_date=start_date,
finish_date=finish_date,
body=body,
)
def build_report(
@@ -120,11 +114,15 @@ def build_report(
rmc=rmc,
url=["/", "raw-material-cost", str(cost_centre.id)],
order=0,
net=Decimal(0),
gross=Decimal(0),
)
)
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", net=Decimal(0), gross=Decimal(0), issue=issues, sale=sales, rmc=rmc, order=0
)
def build_report_id(
@@ -134,28 +132,6 @@ def build_report_id(
sum_net = func.sum(Inventory.rate * Inventory.quantity * Journal.debit).label("net")
sum_gross = func.sum(Inventory.amount * Journal.debit).label("gross")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
query: list[tuple[ProductVersion, ProductGroup, Decimal, Decimal, Decimal]] = db.execute(
select(ProductVersion, ProductGroup, sum_quantity, sum_net, sum_gross)
.select_from(Voucher)
@@ -163,9 +139,9 @@ def build_report_id(
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= start_date,
+42 -47
View File
@@ -1,20 +1,21 @@
import uuid
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Security
from sqlalchemy import not_
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.sql.expression import and_, func, or_, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import report_finish_date, report_start_date
from ...schemas import reconcile as schemas
from ...schemas.account import AccountLink
from ...schemas.user import UserToken
@@ -25,12 +26,13 @@ router = APIRouter()
@router.get("", response_model=schemas.Reconcile)
def show_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["reconcile"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["reconcile"])],
) -> schemas.Reconcile:
return schemas.Reconcile(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
start_date=start_date,
finish_date=finish_date,
account=None,
body=[],
)
@@ -39,23 +41,19 @@ def show_blank(
@router.get("/{id_}", response_model=schemas.Reconcile)
def show_data(
id_: uuid.UUID,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["reconcile"]),
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["reconcile"])],
db: SessionDep,
) -> schemas.Reconcile:
with SessionFuture() as db:
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
start_date = datetime.strptime(s or get_start_date(request.session), "%d-%b-%Y")
finish_date = datetime.strptime(f or get_finish_date(request.session), "%d-%b-%Y")
body = build_report(account.id, start_date, finish_date, db)
set_period(start_date, finish_date, request.session)
return schemas.Reconcile(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
body = build_report(account.id, start_date, finish_date, db)
return schemas.Reconcile(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
def build_report(
@@ -155,28 +153,25 @@ def opening_balance(account_id: uuid.UUID, start_date: date, db: Session) -> sch
def save(
id_: uuid.UUID,
data: schemas.Reconcile,
request: Request,
s: str | None = None,
f: str | None = None,
user: UserToken = Security(get_user, scopes=["reconcile"]),
user: Annotated[UserToken, Security(get_user, scopes=["reconcile"])],
db: SessionDep,
start_date: Annotated[date, Depends(report_start_date)],
finish_date: Annotated[date, Depends(report_finish_date)],
) -> schemas.Reconcile:
with SessionFuture() as db:
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
start_date = datetime.strptime(s or get_start_date(request.session), "%d-%b-%Y")
finish_date = datetime.strptime(f or get_finish_date(request.session), "%d-%b-%Y")
for item in data.body:
if item.id_ is None:
continue
account = db.execute(select(AccountBase).where(AccountBase.id == id_)).scalar_one()
for item in data.body:
if item.id_ is None:
continue
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == item.id_)).scalar_one()
voucher.is_reconciled = item.is_reconciled
if item.reconcile_date is not None:
voucher.reconcile_date = item.reconcile_date
db.commit()
body = build_report(account.id, start_date, finish_date, db)
return schemas.Reconcile(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == item.id_)).scalar_one()
voucher.is_reconciled = item.is_reconciled
if item.reconcile_date is not None:
voucher.reconcile_date = item.reconcile_date
db.commit()
body = build_report(account.id, start_date, finish_date, db)
return schemas.Reconcile(
start_date=start_date,
finish_date=finish_date,
account=AccountLink(id_=account.id, name=account.name),
body=body,
)
@@ -1,14 +1,13 @@
from datetime import date, datetime
from datetime import date
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Request, Security
from sqlalchemy import and_, or_
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.batch import Batch
from ...models.cost_centre import CostCentre
from ...models.inventory import Inventory
@@ -22,6 +21,8 @@ from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...schemas import stock_movement as schemas
from ...schemas.user import UserToken
from .. import _pv_onclause, _sv_onclause
from . import op_report_finish_date, op_report_start_date, report_finish_date, report_start_date
router = APIRouter()
@@ -30,54 +31,30 @@ router = APIRouter()
@router.get("", response_model=schemas.StockMovement)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["stock-movement"]),
start_date: Annotated[date | None, Depends(op_report_start_date)],
finish_date: Annotated[date | None, Depends(op_report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["stock-movement"])],
db: SessionDep,
) -> schemas.StockMovement:
return schemas.StockMovement(
start_date=get_start_date(request.session),
finish_date=get_finish_date(request.session),
body=[],
)
@router.get("/{start}/{finish}", response_model=schemas.StockMovement)
def report_data(
start: str,
finish: str,
request: Request,
user: UserToken = Security(get_user, scopes=["stock-movement"]),
) -> schemas.StockMovement:
with SessionFuture() as db:
body = build_stock_movement(datetime.strptime(start, "%d-%b-%Y"), datetime.strptime(finish, "%d-%b-%Y"), db)
set_period(start, finish, request.session)
return schemas.StockMovement(start_date=start, finish_date=finish, body=body)
if start_date is not None and finish_date is not None:
body = build_stock_movement(start_date, finish_date, db)
return schemas.StockMovement(
start_date=start_date,
finish_date=finish_date,
body=body,
)
else:
return schemas.StockMovement(
start_date=report_start_date(request, None),
finish_date=report_finish_date(request, None),
body=[],
)
def build_stock_movement(start_date: date, finish_date: date, db: Session) -> list[schemas.StockMovementItem]:
dict_ = {}
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
product_version_onclause = and_(
ProductVersion.product_id == Product.id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= Voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= Voucher.date_,
),
)
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= Voucher.date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= Voucher.date_,
),
)
openings: list[tuple[SkuVersion, Decimal]] = db.execute(
select(SkuVersion, quantity_sum)
.select_from(Voucher)
@@ -85,9 +62,9 @@ def build_stock_movement(start_date: date, finish_date: date, db: Session) -> li
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.where(Voucher.date_ < start_date, Journal.cost_centre_id == CostCentre.cost_centre_purchase())
.group_by(StockKeepingUnit, Product, ProductVersion, ProductGroup, SkuVersion) # type: ignore
@@ -110,9 +87,9 @@ def build_stock_movement(start_date: date, finish_date: date, db: Session) -> li
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= start_date,
@@ -143,9 +120,9 @@ def build_stock_movement(start_date: date, finish_date: date, db: Session) -> li
.join(Voucher.inventories)
.join(Inventory.batch)
.join(Batch.sku)
.join(SkuVersion, onclause=sku_version_onclause)
.join(SkuVersion, onclause=_sv_onclause(Voucher.date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion, onclause=_pv_onclause(Voucher.date_))
.join(ProductVersion.product_group)
.where(
Voucher.date_ >= start_date,
@@ -1,17 +1,18 @@
from datetime import date, datetime
from datetime import date
from typing import Annotated
from fastapi import APIRouter, Request, Security
from fastapi import APIRouter, Depends, Request, Security
from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.expression import func, select
from ...core.security import get_current_active_user as get_user
from ...core.session import get_finish_date, get_start_date, set_period
from ...db.session import SessionFuture
from ...db.session import SessionDep
from ...models.account_base import AccountBase
from ...models.account_type import AccountType
from ...models.journal import Journal
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...routers.reports import path_date, report_finish_date
from ...schemas import trial_balance as schemas
from ...schemas.user import UserToken
@@ -21,24 +22,24 @@ router = APIRouter()
@router.get("", response_model=schemas.TrialBalance)
def report_blank(
request: Request,
user: UserToken = Security(get_user, scopes=["trial-balance"]),
finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["trial-balance"])],
) -> schemas.TrialBalance:
return schemas.TrialBalance(date_=get_finish_date(request.session), body=[])
return schemas.TrialBalance(date_=finish_date, body=[])
@router.get("/{date_}", response_model=schemas.TrialBalance)
def report_data(
date_: str,
request: Request,
user: UserToken = Security(get_user, scopes=["trial-balance"]),
date_: Annotated[date, Depends(path_date)],
user: Annotated[UserToken, Security(get_user, scopes=["trial-balance"])],
db: SessionDep,
) -> schemas.TrialBalance:
set_period(get_start_date(request.session), date_, request.session)
with SessionFuture() as db:
return schemas.TrialBalance(
date_=date_,
body=build_report(datetime.strptime(date_, "%d-%b-%Y"), db),
)
report_finish_date(request, date_.strftime("%d-%b-%Y"))
return schemas.TrialBalance(
date_=date_,
body=build_report(date_, db),
)
def build_report(date_: date, db: Session) -> list[schemas.TrialBalanceItem]:
+47 -68
View File
@@ -1,15 +1,15 @@
import uuid
from collections.abc import Sequence
from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from sqlalchemy.sql.functions import count
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.permission import Permission
from ..models.role import Role
from ..models.role_permission import RolePermission
@@ -24,41 +24,27 @@ router = APIRouter()
@router.post("", response_model=schemas.Role)
def save(
data: schemas.RoleIn,
user: UserToken = Security(get_user, scopes=["users"]),
data: schemas.RoleIn, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> schemas.Role:
try:
with SessionFuture() as db:
item = Role(data.name)
db.add(item)
add_permissions(item, data.permissions, db)
db.commit()
return role_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = Role(data.name)
db.add(item)
add_permissions(item, data.permissions, db)
db.commit()
return role_info(item, db)
@router.put("/{id_}", response_model=schemas.Role)
def update_route(
id_: uuid.UUID,
data: schemas.RoleIn,
user: UserToken = Security(get_user, scopes=["users"]),
user: Annotated[UserToken, Security(get_user, scopes=["users"])],
db: SessionDep,
) -> schemas.Role:
try:
with SessionFuture() as db:
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
item.name = data.name
add_permissions(item, data.permissions, db)
db.commit()
return role_info(item, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
item.name = data.name
add_permissions(item, data.permissions, db)
db.commit()
return role_info(item, db)
def add_permissions(role: Role, permissions: list[PermissionItem], db: Session) -> None:
@@ -72,59 +58,52 @@ def add_permissions(role: Role, permissions: list[PermissionItem], db: Session)
@router.delete("/{id_}", response_model=schemas.RoleBlank)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["users"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> schemas.RoleBlank:
with SessionFuture() as db:
if db.execute(select(count(UserRole.id)).where(UserRole.role_id == id_)).scalar_one() > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="This Role has been assigned to users and cannot be deleted.",
)
if db.execute(select(count(RolePermission.id)).where(RolePermission.role_id == id_)).scalar_one() > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="This Role has permissions and cannot be deleted.",
)
# item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
db.delete(item)
db.commit()
return role_blank(db)
if db.execute(select(count(UserRole.id)).where(UserRole.role_id == id_)).scalar_one() > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="This Role has been assigned to users and cannot be deleted.",
)
if db.execute(select(count(RolePermission.id)).where(RolePermission.role_id == id_)).scalar_one() > 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="This Role has permissions and cannot be deleted.",
)
# item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
db.delete(item)
db.commit()
return role_blank(db)
@router.get("", response_model=schemas.RoleBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["users"]),
) -> schemas.RoleBlank:
with SessionFuture() as db:
return role_blank(db)
def show_blank(user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep) -> schemas.RoleBlank:
return role_blank(db)
@router.get("/list", response_model=list[schemas.RoleList])
async def show_list(
user: UserToken = Security(get_user, scopes=["users"]),
user: Annotated[UserToken, Security(get_user, scopes=["users"])],
db: SessionDep,
) -> list[schemas.RoleList]:
with SessionFuture() as db:
roles: Sequence[Role] = db.execute(select(Role).order_by(Role.name)).scalars().all()
return [
schemas.RoleList(
id_=item.id,
name=item.name,
permissions=[p.name for p in sorted(item.permissions, key=lambda p: p.name)],
)
for item in roles
]
roles: Sequence[Role] = db.execute(select(Role).order_by(Role.name)).scalars().all()
return [
schemas.RoleList(
id_=item.id,
name=item.name,
permissions=[p.name for p in sorted(item.permissions, key=lambda p: p.name)],
)
for item in roles
]
@router.get("/{id_}", response_model=schemas.Role)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["users"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> schemas.Role:
with SessionFuture() as db:
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
return role_info(item, db)
item: Role = db.execute(select(Role).where(Role.id == id_)).scalar_one()
return role_info(item, db)
def role_info(item: Role, db: Session) -> schemas.Role:
+55 -86
View File
@@ -1,13 +1,14 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, Security, status
from typing import Annotated
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import delete, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.recipe_tag import RecipeTag
from ..models.tag import Tag
from ..models.voucher import Voucher
@@ -22,132 +23,100 @@ router = APIRouter()
@router.post("", response_model=schemas.Tag)
def save(
data: schemas.Tag,
user: UserToken = Security(get_user, scopes=["tags"]),
data: schemas.Tag, user: Annotated[UserToken, Security(get_user, scopes=["tags"])], db: SessionDep
) -> schemas.Tag:
try:
with SessionFuture() as db:
if len(data.name) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tag name cannot be empty",
)
item = Tag(name=data.name)
db.add(item)
db.commit()
return tag_info(item)
except SQLAlchemyError as e:
if len(data.name) == 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tag name cannot be empty",
)
item = Tag(name=data.name)
db.add(item)
db.commit()
return tag_info(item)
@router.put("/{id_}", response_model=schemas.Tag)
def update_route(
id_: uuid.UUID,
data: schemas.Tag,
user: UserToken = Security(get_user, scopes=["tags"]),
id_: uuid.UUID, data: schemas.Tag, user: Annotated[UserToken, Security(get_user, scopes=["tags"])], db: SessionDep
) -> schemas.Tag:
try:
with SessionFuture() as db:
item = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
if len(data.name) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tag name cannot be empty",
)
item.name = data.name
db.commit()
return tag_info(item)
except SQLAlchemyError as e:
item = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
if len(data.name) == 0:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tag name cannot be empty",
)
item.name = data.name
db.commit()
return tag_info(item)
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["tags"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["tags"])], db: SessionDep
) -> None:
with SessionFuture() as db:
item: Tag = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
db.execute(delete(VoucherTag).where(VoucherTag.tag_id == item.id))
db.execute(delete(RecipeTag).where(RecipeTag.tag_id == item.id))
db.execute(delete(Tag).where(Tag.id == item.id))
db.commit()
item: Tag = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
db.execute(delete(VoucherTag).where(VoucherTag.tag_id == item.id))
db.execute(delete(RecipeTag).where(RecipeTag.tag_id == item.id))
db.execute(delete(Tag).where(Tag.id == item.id))
db.commit()
@router.get("", response_model=schemas.Tag)
def show_blank(
user: UserToken = Security(get_user, scopes=["tags"]),
user: Annotated[UserToken, Security(get_user, scopes=["tags"])],
) -> schemas.Tag:
return tag_blank()
@router.get("/list", response_model=list[schemas.Tag])
async def show_list(user: UserToken = Depends(get_user)) -> list[schemas.Tag]:
with SessionFuture() as db:
return [tag_info(item) for item in db.execute(select(Tag).order_by(Tag.name)).scalars().all()]
async def show_list(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.Tag]:
return [tag_info(item) for item in db.execute(select(Tag).order_by(Tag.name)).scalars().all()]
@router.get("/query", response_model=list[schemas.Tag])
async def show_term(
q: str,
current_user: UserToken = Depends(get_user),
user: Annotated[UserToken, Security(get_user)],
db: SessionDep,
) -> list[schemas.Tag]:
with SessionFuture() as db:
query_ = select(Tag)
for name in q.split():
query_ = query_.where(Tag.name.ilike(f"%{name}%"))
query_ = query_.order_by(Tag.name)
data: list[Tag] = list(db.execute(query_).scalars().all())
query_ = select(Tag)
for name in q.split():
query_ = query_.where(Tag.name.ilike(f"%{name}%"))
query_ = query_.order_by(Tag.name)
data: list[Tag] = list(db.execute(query_).scalars().all())
return [tag_info(item) for item in data]
@router.get("/{id_}", response_model=schemas.Tag)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["tags"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["tags"])], db: SessionDep
) -> schemas.Tag:
with SessionFuture() as db:
item = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
return tag_info(item)
item = db.execute(select(Tag).where(Tag.id == id_)).scalar_one()
return tag_info(item)
@router.post("/vouchers", response_model=bool)
def tag_vouchers(
data: TagVouchers,
user: UserToken = Security(get_user, scopes=["tags"]),
data: TagVouchers, user: Annotated[UserToken, Security(get_user, scopes=["tags"])], db: SessionDep
) -> bool:
try:
with SessionFuture() as db:
old_tags = db.execute(select(Tag.name)).scalars().all()
new_tag = next((t.name for t in data.tags if t.name not in old_tags), None)
if new_tag is not None and new_tag != "":
db.add(Tag(name=new_tag))
db.flush()
remove = [t.name for t in data.tags if t.enabled is False]
tags = db.execute(select(Tag.id).where(Tag.name.in_(remove))).scalars().all()
db.execute(
delete(VoucherTag).where(VoucherTag.tag_id.in_(tags), VoucherTag.voucher_id.in_(data.voucher_ids))
)
old_tags = db.execute(select(Tag.name)).scalars().all()
new_tag = next((t.name for t in data.tags if t.name not in old_tags), None)
if new_tag is not None and new_tag != "":
db.add(Tag(name=new_tag))
db.flush()
remove = [t.name for t in data.tags if t.enabled is False]
tags = db.execute(select(Tag.id).where(Tag.name.in_(remove))).scalars().all()
db.execute(delete(VoucherTag).where(VoucherTag.tag_id.in_(tags), VoucherTag.voucher_id.in_(data.voucher_ids)))
additional = [t.name for t in data.tags if t.enabled is True]
tags = db.execute(select(Tag.id).where(Tag.name.in_(additional))).scalars().all()
for vid in data.voucher_ids:
for t in tags:
db.execute(pg_insert(VoucherTag).values(voucher_id=vid, tag_id=t).on_conflict_do_nothing())
db.commit()
return True
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
additional = [t.name for t in data.tags if t.enabled is True]
tags = db.execute(select(Tag.id).where(Tag.name.in_(additional))).scalars().all()
for vid in data.voucher_ids:
for t in tags:
db.execute(pg_insert(VoucherTag).values(voucher_id=vid, tag_id=t).on_conflict_do_nothing())
db.commit()
return True
def tag_info(item: Tag) -> schemas.Tag:
+66 -102
View File
@@ -1,14 +1,14 @@
import uuid
from collections.abc import Sequence
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import delete, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.attendance import Attendance
from ..models.login_history import LoginHistory
from ..models.role import Role
@@ -25,76 +25,49 @@ router = APIRouter()
@router.post("", response_model=schemas.User)
def save(
data: schemas.UserIn,
user: UserToken = Security(get_user, scopes=["users"]),
data: schemas.UserIn, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> schemas.User:
try:
with SessionFuture() as db:
item = User(name=data.name, password=data.password, locked_out=data.locked_out)
db.add(item)
add_roles(item, data.roles, db)
db.commit()
return user_info(item, db, user)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item = User(name=data.name, password=data.password, locked_out=data.locked_out)
db.add(item)
add_roles(item, data.roles, db)
db.commit()
return user_info(item, db, user)
@router.get("/me", response_model=schemas.User)
def show_me(
user: UserToken = Depends(get_user),
) -> schemas.User:
with SessionFuture() as db:
item = db.execute(select(User).where(User.id == user.id_)).scalar_one()
return user_info(item, db, user)
def show_me(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> schemas.User:
item = db.execute(select(User).where(User.id == user.id_)).scalar_one()
return user_info(item, db, user)
@router.put("/me", response_model=schemas.User)
def update_me(
data: schemas.UserIn,
user: UserToken = Depends(get_user),
) -> schemas.User:
try:
with SessionFuture() as db:
item: User = db.execute(select(User).where(User.id == user.id_)).scalar_one()
if "users" in user.permissions:
item.name = data.name
item.locked_out = data.locked_out
add_roles(item, data.roles, db)
if data.password and item.password != data.password:
item.password = data.password
db.commit()
return user_info(item, db, user)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
def update_me(data: schemas.UserIn, user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> schemas.User:
item: User = db.execute(select(User).where(User.id == user.id_)).scalar_one()
if "users" in user.permissions:
item.name = data.name
item.locked_out = data.locked_out
add_roles(item, data.roles, db)
if data.password and item.password != data.password:
item.password = data.password
db.commit()
return user_info(item, db, user)
@router.put("/{id_}", response_model=schemas.User)
def update_route(
id_: uuid.UUID,
data: schemas.UserIn,
user: UserToken = Security(get_user, scopes=["users"]),
user: Annotated[UserToken, Security(get_user, scopes=["users"])],
db: SessionDep,
) -> schemas.User:
try:
with SessionFuture() as db:
item: User = db.execute(select(User).where(User.id == id_)).scalar_one()
item.name = data.name
if data.password and item.password != data.password:
item.password = data.password
item.locked_out = data.locked_out
add_roles(item, data.roles, db)
db.commit()
return user_info(item, db, user)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
item: User = db.execute(select(User).where(User.id == id_)).scalar_one()
item.name = data.name
if data.password and item.password != data.password:
item.password = data.password
item.locked_out = data.locked_out
add_roles(item, data.roles, db)
db.commit()
return user_info(item, db, user)
def add_roles(user: User, roles: list[RoleItem], db: Session) -> None:
@@ -108,71 +81,62 @@ def add_roles(user: User, roles: list[RoleItem], db: Session) -> None:
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["users"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> None:
if id_ == user.id_:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="You cannot delete yourself",
)
with SessionFuture() as db:
item: User = db.execute(select(User).where(User.id == id_)).scalar_one()
admin: User = db.execute(select(User).where(User.name.ilike("admin"))).scalar_one()
db.execute(update(Voucher).where(Voucher.user_id == item.id).values(user_id=admin.id))
db.execute(update(Attendance).where(Attendance.user_id == item.id).values(user_id=admin.id))
db.execute(delete(LoginHistory).where(LoginHistory.user_id == item.id))
db.execute(delete(UserRole).where(UserRole.user_id == item.id))
db.execute(delete(User).where(User.id == item.id))
db.commit()
item: User = db.execute(select(User).where(User.id == id_)).scalar_one()
admin: User = db.execute(select(User).where(User.name.ilike("admin"))).scalar_one()
db.execute(update(Voucher).where(Voucher.user_id == item.id).values(user_id=admin.id))
db.execute(update(Attendance).where(Attendance.user_id == item.id).values(user_id=admin.id))
db.execute(delete(LoginHistory).where(LoginHistory.user_id == item.id))
db.execute(delete(UserRole).where(UserRole.user_id == item.id))
db.execute(delete(User).where(User.id == item.id))
db.commit()
@router.get("", response_model=schemas.UserBlank)
def show_blank(
user: UserToken = Security(get_user, scopes=["users"]),
) -> schemas.UserBlank:
with SessionFuture() as db:
return user_blank(db)
def show_blank(user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep) -> schemas.UserBlank:
return user_blank(db)
@router.get("/list", response_model=list[schemas.UserList])
async def show_list(
user: UserToken = Security(get_user, scopes=["users"]),
user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> list[schemas.UserList]:
with SessionFuture() as db:
users: Sequence[User] = db.execute(select(User).order_by(User.name)).scalars().all()
return [
schemas.UserList(
id_=item.id,
name=item.name,
locked_out=item.locked_out,
roles=[p.name for p in sorted(item.roles, key=lambda p: p.name)],
last_device=item.login_history[0].client.name if len(item.login_history) else "None",
last_date=item.login_history[0].date if len(item.login_history) else None,
)
for item in users
]
users: Sequence[User] = db.execute(select(User).order_by(User.name)).scalars().all()
return [
schemas.UserList(
id_=item.id,
name=item.name,
locked_out=item.locked_out,
roles=[p.name for p in sorted(item.roles, key=lambda p: p.name)],
last_device=item.login_history[0].client.name if len(item.login_history) else "None",
last_date=item.login_history[0].date if len(item.login_history) else None,
)
for item in users
]
@router.get("/active", response_model=list[schemas.UserBlank])
async def show_active(user: UserToken = Depends(get_user)) -> list[schemas.UserBlank]:
with SessionFuture() as db:
return [
schemas.UserBlank(name=name, password="", locked_out=False, roles=[])
for name in db.execute(select(User.name).where(User.locked_out == False).order_by(User.name)) # noqa: E712
.scalars()
.all()
]
async def show_active(user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> list[schemas.UserBlank]:
return [
schemas.UserBlank(name=name, password="", locked_out=False, roles=[])
for name in db.execute(select(User.name).where(User.locked_out == False).order_by(User.name)) # noqa: E712
.scalars()
.all()
]
@router.get("/{id_}", response_model=schemas.User)
def show_id(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["users"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["users"])], db: SessionDep
) -> schemas.User:
with SessionFuture() as db:
item = db.execute(select(User).where(User.id == id_)).scalar_one()
return user_info(item, db, user)
item = db.execute(select(User).where(User.id == id_)).scalar_one()
return user_info(item, db, user)
def user_info(item: User, db: Session, user: UserToken) -> schemas.User:
+102 -163
View File
@@ -3,16 +3,14 @@ import uuid
from collections.abc import Sequence
from datetime import date
from decimal import Decimal
from typing import cast
from typing import Annotated, cast
from fastapi import APIRouter, HTTPException, Security, status
from sqlalchemy import and_, distinct, func, or_, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..core.session import get_first_day
from ..db.session import SessionFuture
from ..db.session import SessionDep
from ..models.account import Account
from ..models.account_base import AccountBase
from ..models.attendance import Attendance
@@ -26,7 +24,7 @@ from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion
from ..models.voucher import Voucher
from ..models.voucher_type import VoucherType
from ..routers import get_lock_info
from ..routers.reports import get_first_day
from ..schemas import voucher as output
from ..schemas.account import AccountLink
from ..schemas.batch import Batch as BatchSchema
@@ -42,6 +40,7 @@ from ..schemas.product import ProductLink
from ..schemas.tag import Tag as TagSchema
from ..schemas.user import UserToken
from ..schemas.user_link import UserLink
from . import _pv_active, _sv_active, get_lock_info
router = APIRouter()
@@ -49,36 +48,26 @@ router = APIRouter()
@router.post("/post-voucher/{id_}", response_model=output.Voucher)
def post_voucher(
id_: uuid.UUID,
user: UserToken = Security(get_user, scopes=["post-vouchers"]),
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["post-vouchers"])], db: SessionDep
) -> output.Voucher:
try:
with SessionFuture() as db:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
account_types: Sequence[int] = (
db.execute(
select(distinct(AccountBase.type_id)).where(
AccountBase.id.in_([vj.account_id for vj in voucher.journals])
)
)
.scalars()
.all()
)
allowed, message = get_lock_info([voucher.date_], voucher.voucher_type, account_types, db)
if not allowed:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=message,
)
voucher.posted = True
voucher.poster_id = user.id_
db.commit()
return voucher_info(voucher, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
account_types: Sequence[int] = (
db.execute(
select(distinct(AccountBase.type_id)).where(AccountBase.id.in_([vj.account_id for vj in voucher.journals]))
)
.scalars()
.all()
)
allowed, message = get_lock_info([voucher.date_], voucher.voucher_type, account_types, db)
if not allowed:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=message,
)
voucher.posted = True
voucher.poster_id = user.id_
db.commit()
return voucher_info(voucher, db)
def check_delete_permissions(voucher: Voucher, user: UserToken) -> None:
@@ -103,125 +92,97 @@ def check_delete_permissions(voucher: Voucher, user: UserToken) -> None:
@router.delete("/delete/{id_}")
def delete_voucher(
id_: uuid.UUID,
user: UserToken = Security(get_user),
) -> output.Voucher:
with SessionFuture() as db:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
images = db.execute(select(DbImage).where(DbImage.resource_id == voucher.id)).scalars().all()
check_delete_permissions(voucher, user)
account_types: Sequence[int] = (
db.execute(
select(distinct(AccountBase.type_id)).where(
AccountBase.id.in_([vj.account_id for vj in voucher.journals])
)
)
.scalars()
.all()
def delete_voucher(id_: uuid.UUID, user: Annotated[UserToken, Security(get_user)], db: SessionDep) -> output.Voucher:
voucher: Voucher = db.execute(select(Voucher).where(Voucher.id == id_)).scalar_one()
images = db.execute(select(DbImage).where(DbImage.resource_id == voucher.id)).scalars().all()
check_delete_permissions(voucher, user)
account_types: Sequence[int] = (
db.execute(
select(distinct(AccountBase.type_id)).where(AccountBase.id.in_([vj.account_id for vj in voucher.journals]))
)
allowed, message = get_lock_info([voucher.date_], voucher.voucher_type, account_types, db)
if not allowed:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=message,
)
json_voucher = voucher_info(voucher, db)
batches_to_delete = []
if voucher.voucher_type == VoucherType.ISSUE:
for j_item in voucher.journals:
if j_item.debit == 1:
destination = j_item.cost_centre_id
else:
source = j_item.cost_centre_id
batch_consumed: bool | None
if source == CostCentre.cost_centre_purchase():
batch_consumed = True
elif destination == CostCentre.cost_centre_purchase():
batch_consumed = False
.scalars()
.all()
)
allowed, message = get_lock_info([voucher.date_], voucher.voucher_type, account_types, db)
if not allowed:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=message,
)
json_voucher = voucher_info(voucher, db)
batches_to_delete = []
if voucher.voucher_type == VoucherType.ISSUE:
for j_item in voucher.journals:
if j_item.debit == 1:
destination = j_item.cost_centre_id
else:
batch_consumed = None
source = j_item.cost_centre_id
if batch_consumed is None:
pass
elif batch_consumed:
for i_item in voucher.inventories:
i_item.batch.quantity_remaining += i_item.quantity
else:
for i_item in voucher.inventories:
if i_item.batch.quantity_remaining < i_item.quantity:
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == i_item.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
)
).scalar_one()
batch_consumed: bool | None
if source == CostCentre.cost_centre_purchase():
batch_consumed = True
elif destination == CostCentre.cost_centre_purchase():
batch_consumed = False
else:
batch_consumed = None
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"Only {i_item.batch.quantity_remaining}"
f" of {product_name} remaining.\n"
f"So it cannot be deleted",
)
i_item.batch.quantity_remaining -= i_item.quantity
elif voucher.voucher_type == VoucherType.PURCHASE:
if batch_consumed is None:
pass
elif batch_consumed:
for i_item in voucher.inventories:
uses: int = db.execute(
select(func.count(Inventory.id)).where(
Inventory.batch_id == i_item.batch.id, Inventory.id != i_item.id
)
).scalar_one()
if uses > 0:
i_item.batch.quantity_remaining += i_item.quantity
else:
for i_item in voucher.inventories:
if i_item.batch.quantity_remaining < i_item.quantity:
product_name = db.execute(
select(ProductVersion.name).where(
and_(
ProductVersion.product_id == i_item.batch.sku.product_id,
or_(
ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= voucher.date_,
),
or_(
ProductVersion.valid_till == None, # noqa: E711
ProductVersion.valid_till >= voucher.date_,
),
)
and_(ProductVersion.product_id == i_item.batch.sku.product_id, _pv_active(voucher.date_))
)
).scalar_one()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{product_name} has been issued and cannot be deleted",
detail=f"Only {i_item.batch.quantity_remaining}"
f" of {product_name} remaining.\n"
f"So it cannot be deleted",
)
batches_to_delete.append(i_item.batch)
elif voucher.voucher_type == VoucherType.PURCHASE_RETURN:
for item in voucher.inventories:
item.batch.quantity_remaining += item.quantity
for b in batches_to_delete:
db.delete(b)
voucher.tags.clear()
db.delete(voucher)
for image in images:
db.delete(image)
db.commit()
i_item.batch.quantity_remaining -= i_item.quantity
elif voucher.voucher_type == VoucherType.PURCHASE:
for i_item in voucher.inventories:
uses: int = db.execute(
select(func.count(Inventory.id)).where(Inventory.batch_id == i_item.batch.id, Inventory.id != i_item.id)
).scalar_one()
if uses > 0:
product_name = db.execute(
select(ProductVersion.name).where(
and_(ProductVersion.product_id == i_item.batch.sku.product_id, _pv_active(voucher.date_))
)
).scalar_one()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{product_name} has been issued and cannot be deleted",
)
batches_to_delete.append(i_item.batch)
elif voucher.voucher_type == VoucherType.PURCHASE_RETURN:
for item in voucher.inventories:
item.batch.quantity_remaining += item.quantity
for b in batches_to_delete:
db.delete(b)
voucher.tags.clear()
db.delete(voucher)
for image in images:
db.delete(image)
db.commit()
return blank_voucher(
info=BlankVoucherInfo(
date_=json_voucher.date_,
type_=VoucherType[json_voucher.type_],
source=json_voucher.source,
destination=json_voucher.destination,
),
db=db,
)
return blank_voucher(
info=BlankVoucherInfo(
date_=json_voucher.date_,
type_=VoucherType[json_voucher.type_],
source=json_voucher.source,
destination=json_voucher.destination,
),
db=db,
)
def voucher_info(voucher: Voucher, db: Session) -> output.Voucher:
@@ -309,33 +270,11 @@ def voucher_info(voucher: Voucher, db: Session) -> output.Voucher:
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_,
),
)
and_(ProductVersion.product_id == inventory.batch.sku.product_id, _pv_active(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_,
),
)
)
select(SkuVersion.units).where(and_(SkuVersion.sku_id == inventory.batch.sku_id, _sv_active(voucher.date_)))
).scalar_one()
text = (
f"{product_name} ({product_unit}) "
+4 -2
View File
@@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends
from typing import Annotated
from fastapi import APIRouter, Security
from ..core.security import get_current_active_user as get_user
from ..models.voucher_type import VoucherType
@@ -10,5 +12,5 @@ router = APIRouter()
@router.get("", response_model=list[schemas.AccountType])
def account_type_list(user: UserToken = Depends(get_user)) -> list[schemas.AccountType]:
def account_type_list(user: Annotated[UserToken, Security(get_user)]) -> list[schemas.AccountType]:
return [schemas.AccountType(id_=item.value, name=item.name) for item in list(VoucherType)]
@@ -0,0 +1,6 @@
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: str
database: str
+2 -2
View File
@@ -21,8 +21,8 @@ class ProfitLossItem(BaseModel):
class ProfitLoss(BaseModel):
start_date: date | str
finish_date: date | str
start_date: date
finish_date: date
body: list[ProfitLossItem]
footer: ProfitLossItem
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
-23
View File
@@ -1,23 +0,0 @@
"""
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 ..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)
+1 -1
View File
@@ -62,4 +62,4 @@ log_data = {
"host": host,
"port": port,
}
print(json.dumps(log_data))
print(json.dumps(log_data)) # noqa: T201
-3239
View File
File diff suppressed because it is too large. Load diff
+64 -64
View File
@@ -1,10 +1,46 @@
[project]
name = "brewman"
version = "14.0.0"
description = "Accounting plus inventory management for a restaurant."
requires-python = ">=3.14"
authors = [{ name = "tanshu", email = "git@tanshu.com" }]
dependencies = [
"alembic>=1.18.4",
"arq>=0.27.0",
"cryptography>=46.0.5",
"fastapi[all]>=0.129.0",
"gunicorn>=25.1.0",
"itsdangerous>=2.2.0",
"openpyxl>=3.1.5",
"pandas>=3.0.1",
"passlib[bcrypt]>=1.7.4",
"psycopg[binary,pool]>=3.3.3",
"pydantic>=2.12.5",
"pydantic-settings>=2.13.1",
"pyjwt>=2.11.0",
"python-dotenv>=1.2.1",
"python-multipart>=0.0.22",
"sqlalchemy>=2.0.46",
"uvicorn[standard]>=0.41.0",
]
[dependency-groups]
dev = [
"bandit>=1.9.3",
"mypy>=1.19.1",
"pre-commit>=4.5.1",
"ruff>=0.15.2",
"safety>=3.7.0",
"types-openpyxl>=3.1.5.20250919",
]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.mypy]
# --strict
# --- Strictness ---
strict = true
# Good safety defaults
warn_return_any = true
warn_unused_configs = true
@@ -12,9 +48,9 @@ warn_redundant_casts = true
warn_unused_ignores = true
no_implicit_optional = true
strict_equality = true
warn_unreachable = true
# Practical ergonomics
pretty = true
show_error_codes = true
show_column_numbers = true
error_summary = true
# These keep signal high in real-world FastAPI projects
@@ -26,41 +62,11 @@ disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_defs = true
disallow_untyped_decorators = true
# enterprise backend strict
# disallow_any_expr = true
# disallow_any_decorated = true
disallow_any_unimported = true
plugins = ["sqlalchemy.ext.mypy.plugin", "pydantic.mypy"]
# --strict end
[tool.poetry]
name = "brewman"
version = "14.0.0"
description = "Accounting plus inventory management for a restaurant."
authors = ["tanshu <git@tanshu.com>"]
[tool.poetry.dependencies]
PyJWT = "^2.11.0"
SQLAlchemy = "^2.0.46"
alembic = "^1.18.4"
arq = "^0.27.0"
cryptography = "^46.0.5"
fastapi = { extras = ["all"], version = "^0.129.0" }
gunicorn = "^25.1.0"
itsdangerous = "^2.2.0"
openpyxl = "^3.1.5"
pandas = "^3.0.1"
passlib = { extras = ["bcrypt"], version = "^1.7.4" }
psycopg = { extras = ["binary", "pool"], version = "^3.3.2" }
pydantic = { extras = ["dotenv"], version = "^2.12.5" }
python = "^3.14"
python-dotenv = "^1.2.1"
python-multipart = "^0.0.22"
uvicorn = { extras = ["standard"], version = "^0.41.0" }
[tool.poetry.group.dev.dependencies]
bandit = "^1.9.3"
mypy = "^1.19.1"
pre-commit = "^4.5.1"
ruff = "^0.15.1"
# safety = "^3.7.0"
types-openpyxl = "^3.1.5.20250919"
[tool.pydantic-mypy]
init_forbid_extra = true
@@ -84,29 +90,27 @@ exclude = [
]
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
# select = ["E4", "E7", "E9", "F"]
select = [
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
# "B",
# flake8-simplify
"SIM",
# isort
"I",
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
"T201", # print statements are not allowed
]
ignore = ["E203", "E501"]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
# Avoid trying to fix flake8-bugbear (`B`) violations.
unfixable = ["B", "F401"]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
fixable = [
"ALL"
] # Allow fix for all enabled rules (when `--fix`) is provided.
dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.isort]
# include-trailing-comma = true
@@ -115,11 +119,7 @@ lines-between-types = 1
# use-parentheses = true
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
gunicorn brewman.main:app --worker-class uvicorn.workers.UvicornWorker --config ./gunicorn.conf.py --log-config ./logging.conf
+1704
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
22.17.0
+12 -22
View File
@@ -7,7 +7,7 @@
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
"style": "sass"
},
"@schematics/angular:application": {
"strict": true
@@ -23,12 +23,16 @@
"outputPath": {
"base": "../frontend"
},
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": [],
"browser": "src/main.ts"
"inlineStyleLanguage": "sass",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.sass"]
},
"configurations": {
"production": {
@@ -49,8 +53,7 @@
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
"sourceMap": true
}
},
"defaultConfiguration": "production"
@@ -70,21 +73,8 @@
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "overlord:build"
}
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
"builder": "@angular/build:unit-test"
},
"lint": {
"builder": "@angular-eslint/builder:lint",
-26
View File
@@ -1,26 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: ['./src/**/*.e2e-spec.ts'],
capabilities: {
browserName: 'chrome',
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () {},
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json'),
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
},
};
-14
View File
@@ -1,14 +0,0 @@
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});
-11
View File
@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": ["jasmine", "jasminewd2", "node"]
}
}
+15 -9
View File
@@ -1,21 +1,29 @@
// @ts-check
const eslint = require('@eslint/js');
const { defineConfig } = require('eslint/config');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
const eslintConfigPrettier = require('eslint-config-prettier');
const unusedImports = require('eslint-plugin-unused-imports');
const perfectionist = require('eslint-plugin-perfectionist');
module.exports = tseslint.config(
module.exports = defineConfig([
{
files: ['**/*.ts'],
ignores: ['**/*.spec.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...angular.configs.tsRecommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
angular.configs.tsRecommended,
eslintConfigPrettier,
],
languageOptions: {
parserOptions: {
project: './tsconfig.app.json', // <-- REQUIRED
tsconfigRootDir: __dirname,
},
},
processor: angular.processInlineTemplates,
plugins: {
unusedImports: unusedImports,
@@ -50,6 +58,7 @@ module.exports = tseslint.config(
'@typescript-eslint/no-explicit-any': ['error'],
'unusedImports/no-unused-imports': 'error',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-deprecated': 'error',
'perfectionist/sort-imports': [
'error',
{
@@ -61,10 +70,7 @@ module.exports = tseslint.config(
},
{
files: ['**/*.html'],
extends: [
...angular.configs.templateRecommended,
// ...angular.configs.templateAccessibility,
],
extends: [angular.configs.templateRecommended, angular.configs.templateAccessibility],
rules: {},
},
);
]);
+20 -31
View File
@@ -14,32 +14,28 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.4",
"@angular/cdk": "^20.0.3",
"@angular/common": "^20.0.4",
"@angular/compiler": "^20.0.4",
"@angular/core": "^20.0.4",
"@angular/forms": "^20.0.4",
"@angular/material": "^20.0.3",
"@angular/material-moment-adapter": "^20.0.3",
"@angular/platform-browser": "^20.0.4",
"@angular/platform-browser-dynamic": "^20.0.4",
"@angular/router": "^20.0.4",
"@ngx-loading-bar/core": "^7.0.0",
"@ngx-loading-bar/http-client": "^7.0.0",
"@ngx-loading-bar/router": "^7.0.0",
"mathjs": "^14.5.2",
"@angular/animations": "^21.1.5",
"@angular/cdk": "^21.1.5",
"@angular/common": "^21.1.5",
"@angular/compiler": "^21.1.5",
"@angular/core": "^21.1.5",
"@angular/forms": "^21.1.5",
"@angular/material": "^21.1.5",
"@angular/material-moment-adapter": "^21.1.5",
"@angular/platform-browser": "^21.1.5",
"@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.1.5",
"mathjs": "^15.1.1",
"moment": "^2.30.1",
"rxjs": "~7.8.0",
"tslib": "^2.6.0"
},
"devDependencies": {
"@angular/build": "^20.0.3",
"@angular/cli": "^20.0.3",
"@angular/compiler-cli": "^20.0.4",
"@angular/language-service": "^20.0.4",
"@types/jasmine": "~5.1.4",
"angular-eslint": "^20.1.1",
"@angular/build": "^21.1.4",
"@angular/cli": "^21.1.4",
"@angular/compiler-cli": "^21.1.5",
"@angular/language-service": "^21.1.5",
"angular-eslint": "^21.2.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0",
@@ -47,16 +43,9 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.0.11",
"jasmine-core": "~5.1.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"lint-staged": "^16.1.2",
"prettier": "^3.2.5",
"standard-version": "^9.5.0",
"typescript": "~5.8.3",
"lint-staged": "^16.2.7",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.33.1"
},
"husky": {
-1
View File
@@ -1,4 +1,3 @@
<ngx-loading-bar></ngx-loading-bar>
<app-nav-bar></app-nav-bar>
<div class="mat-app-background basic-container">
<router-outlet></router-outlet>
File renamed without changes.
+2 -3
View File
@@ -1,14 +1,13 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoadingBarModule } from '@ngx-loading-bar/core';
import { NavBarComponent } from './core/nav-bar/nav-bar.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
imports: [LoadingBarModule, NavBarComponent, RouterOutlet],
styleUrls: ['./app.component.sass'],
imports: [NavBarComponent, RouterOutlet],
})
export class AppComponent {
title = 'app';
-3
View File
@@ -18,7 +18,6 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatToolbarModule } from '@angular/material/toolbar';
import { BrowserModule } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withRouterConfig } from '@angular/router';
import { dateFormat } from './app.environment';
@@ -34,7 +33,6 @@ export const appConfig: ApplicationConfig = {
BrowserModule,
LayoutModule,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatInputModule,
@@ -52,7 +50,6 @@ export const appConfig: ApplicationConfig = {
{ provide: LOCALE_ID, useValue: 'en-IN' },
AuthService,
provideHttpClient(withFetch(), withInterceptors([refreshInterceptor, jwtInterceptor, authInterceptor])),
provideAnimations(),
provideRouter(
routes,
withRouterConfig({
@@ -65,7 +65,7 @@ export class AttendanceComponent implements OnInit {
displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: KeyboardEvent) {
focusDate(event: Event) {
event.preventDefault();
this.dateElement.nativeElement.focus();
this.dateElement.nativeElement.select();
@@ -49,7 +49,7 @@ export class BalanceSheetComponent implements OnInit {
displayedColumns = ['group', 'name', 'subAmount', 'total'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: KeyboardEvent) {
focusDate(event: Event) {
event.preventDefault();
this.dateElement.nativeElement.focus();
this.dateElement.nativeElement.select();
@@ -47,7 +47,7 @@ export class CashFlowComponent implements OnInit {
displayedColumns = ['name', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: KeyboardEvent) {
focusDate(event: Event) {
event.preventDefault();
this.startDate.nativeElement.focus();
this.startDate.nativeElement.select();
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more