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:
1 parent
770fd0c6dd
commit
1e7476c5d9
148 files changed
+7600
-11635
No files matched your search
Vendored
+3
-3
@@ -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
@@ -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"]
|
||||
@@ -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 \
|
||||
-
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -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_
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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_:
|
||||
|
||||
@@ -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_
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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]))
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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=[],
|
||||
)
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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]:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]))
|
||||
)
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
],
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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}) "
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -62,4 +62,4 @@ log_data = {
|
||||
"host": host,
|
||||
"port": port,
|
||||
}
|
||||
print(json.dumps(log_data))
|
||||
print(json.dumps(log_data)) # noqa: T201
|
||||
Generated
-3239
File diff suppressed because it is too large.
Load diff
+64
-64
@@ -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"
|
||||
@@ -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
|
||||
Generated
+1704
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
22.17.0
|
||||
+12
-22
@@ -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",
|
||||
|
||||
@@ -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 } }));
|
||||
},
|
||||
};
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": ["jasmine", "jasminewd2", "node"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,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.
@@ -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';
|
||||
|
||||
@@ -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
Reference in new issue
Block a user