Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dfbfd8fcb | ||
|
|
221fadfdfc | ||
|
|
11b93cb11c | ||
|
|
302a4eb311 | ||
|
|
217f636573 | ||
|
|
95ac73bbc4 | ||
|
|
3d86f8706f | ||
|
|
30cbb60efe | ||
|
|
227f5b5f74 | ||
|
|
50974c7574 | ||
|
|
c8e1ab74b6 | ||
|
|
85ba7b326a | ||
|
|
458dfaad50 | ||
|
|
0a9675f270 | ||
|
|
6cf42d3641 | ||
|
|
cf680cd254 | ||
|
|
80d21ec31e | ||
|
|
4377016cda | ||
|
|
67209886b9 | ||
|
|
4e1c0d1ec7 | ||
|
|
c7412f080d | ||
|
|
d023089720 | ||
|
|
dd02a97ba2 | ||
|
|
befd4eb298 | ||
|
|
d90c1cb563 | ||
|
|
e90c80999e | ||
|
|
42331b482d | ||
|
|
612e2243c0 | ||
|
|
35a3bb9ad2 | ||
|
|
ae69c2fafe | ||
|
|
85a063c1a7 | ||
|
|
10448e8b12 | ||
|
|
59bf2c5ca9 | ||
|
|
73fba17711 | ||
|
|
103983846d | ||
|
|
af07bb5043 | ||
|
|
612426b92f | ||
|
|
730048f239 | ||
|
|
ff537edb06 | ||
|
|
311464e593 | ||
|
|
43204e8441 | ||
|
|
2a29dc9195 | ||
|
|
65527dcbff | ||
|
|
cc52670783 | ||
|
|
993f83c786 | ||
|
|
59eb45da96 | ||
|
|
ebe5a6352e | ||
|
|
38a8656008 | ||
|
|
2e5543a2eb | ||
|
|
34e30efcaf | ||
|
|
2b844bc44b | ||
|
|
325bcae998 | ||
|
|
ab8fed17d0 | ||
|
|
dcf1ffad98 | ||
|
|
338d9d63d5 | ||
|
|
9b0da9cb65 | ||
|
|
4cc2ff2229 | ||
|
|
c9fa83e8ac | ||
|
|
061dd1310d | ||
|
|
a1d6919d9d | ||
|
|
ecc92795e6 | ||
|
|
535a145742 | ||
|
|
1271e26dd8 | ||
|
|
2ae3f3ba7e | ||
|
|
e6a03d33e5 | ||
|
|
c7dd6e574d | ||
|
|
913820cc29 | ||
|
|
e3d40d50f0 | ||
|
|
b27a5a9211 | ||
|
|
a955e26b93 | ||
|
|
bd2b1f5c2f | ||
|
|
0fa28019ec | ||
|
|
ddb03ac8eb | ||
|
|
ae8975b29f | ||
|
|
e7e49019c4 | ||
|
|
dc34a78e06 | ||
|
|
91b3740ec0 | ||
|
|
7382d487ac | ||
|
|
8957e4ce8c | ||
|
|
6e8843d3c4 | ||
|
|
b79af6e1c6 | ||
|
|
c65517b377 | ||
|
|
ca94f8f57e | ||
|
|
e0766c5a2e | ||
|
|
bd1a5e03c5 | ||
|
|
61f5f2f1af | ||
|
|
0a7ffb4a5c | ||
|
|
22f888500f | ||
|
|
2505ef7aec | ||
|
|
4f0346551a | ||
|
|
e4072d0872 | ||
|
|
1b8ab2857f | ||
|
|
aaf64ab75a | ||
|
|
6caeb34e8d | ||
|
|
88b53b5781 | ||
|
|
8ff7363fc8 | ||
|
|
e1cbf7ffb8 | ||
|
|
b33468a43f | ||
|
|
3e8f2e9736 | ||
|
|
f2d5d62aa6 | ||
|
|
eee9ae1f11 | ||
|
|
2fbfe96c40 | ||
|
|
01b0cbfadf | ||
|
|
88a1572747 | ||
|
|
5c1c474e93 | ||
|
|
74e17dffd0 | ||
|
|
8ad9e6562c | ||
|
|
d80d097c0e | ||
|
|
0dbec4784b | ||
|
|
af684c976e | ||
|
|
26c65b4b65 | ||
|
|
2c89e218c8 | ||
|
|
b2732434f4 | ||
|
|
5317e3de23 | ||
|
|
add3ad35d7 | ||
|
|
feb867fe28 | ||
|
|
fc2ef36a0c | ||
|
|
31b4ee1ebf | ||
|
|
0e9d9c5934 | ||
|
|
5401318fc1 | ||
|
|
66834b7399 | ||
|
|
44513dd6be | ||
|
|
86722e3558 | ||
|
|
2ca94e5bea | ||
|
|
ad8ccb26df | ||
|
|
abc1b36721 | ||
|
|
68ab90ec48 | ||
|
|
7be7545137 | ||
|
|
b46fd10f5a | ||
|
|
50846ace3f | ||
|
|
e36de39ed5 | ||
|
|
4b1fca0260 | ||
|
|
43a884144b | ||
|
|
85a939b761 | ||
|
|
3bfa00364c | ||
|
|
0b092b5467 | ||
|
|
82747c4b89 | ||
|
|
e732354671 | ||
|
|
6d3057bb55 | ||
|
|
15a93c599e | ||
|
|
823e3a761e | ||
|
|
ac728128b9 | ||
|
|
5536bcae41 | ||
|
|
55d5017254 | ||
|
|
bbb7be8070 | ||
|
|
b1407f339d | ||
|
|
659fed549a | ||
|
|
a5efcc9061 | ||
|
|
7e70214c94 | ||
|
|
2495c24e1a | ||
|
|
010e9a84db | ||
|
|
56c1be5e05 | ||
|
|
cbd5da12cc | ||
|
|
93b1b8840d | ||
|
|
ecea277e46 | ||
|
|
1cb2677ad1 | ||
|
|
e8a33cd80e | ||
|
|
cb3657615a | ||
|
|
e955ef4196 | ||
|
|
f1ef578eae | ||
|
|
05302c4291 | ||
|
|
96e54e2a0a | ||
|
|
26310dfc42 | ||
|
|
16f29c4139 | ||
|
|
f8c1e0591b | ||
|
|
42102d7fef | ||
|
|
09b762d154 | ||
|
|
8b2d15b9e1 | ||
|
|
5534b3983d | ||
|
|
e685941804 | ||
|
|
8bc7d66123 | ||
|
|
302ed4a18f | ||
|
|
2fb2e01ca1 | ||
|
|
94cc8ccd47 | ||
|
|
c13047e812 | ||
|
|
5c697dd43a | ||
|
|
56ae7500cc | ||
|
|
1b4c26733d | ||
|
|
88bda14002 | ||
|
|
7a8017087b | ||
|
|
1297a4e7d2 | ||
|
|
47afb7e29c | ||
|
|
4db26722a3 | ||
|
|
b2db7e8f9c | ||
|
|
f6deea265c | ||
|
|
f1d145e76f | ||
|
|
22f61427b0 | ||
|
|
f9fc6dba0d | ||
|
|
e56a753053 | ||
|
|
03918fee80 | ||
|
|
c14e64ce85 | ||
|
|
4e4cebf5d4 | ||
|
|
d39712a347 | ||
|
|
40a357edc8 | ||
|
|
1a08066c2d | ||
|
|
a62018ad1f | ||
|
|
da46591313 | ||
|
|
a1ccf973cd | ||
|
|
65700d2760 | ||
|
|
77c7b167ef | ||
|
|
efa2af396d | ||
|
|
b021861ba3 | ||
|
|
245937ecf2 | ||
|
|
3af0b0ac96 | ||
|
|
4fffdd85d6 | ||
|
|
7da9212cf7 | ||
|
|
0f1660ef5a | ||
|
|
7d0874093c | ||
|
|
516b22ed1c | ||
|
|
c5eaeb1497 | ||
|
|
f408e84af3 | ||
|
|
e46fe7f90e | ||
|
|
802eded568 | ||
|
|
5c7985e392 | ||
|
|
792ccf923f | ||
|
|
f637f01954 | ||
|
|
236c746e1c | ||
|
|
a4db3b24f0 | ||
|
|
9f4ec3697c | ||
|
|
9fe84800a7 |
No files matched your search
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres-db": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-postgres",
|
||||
"${POSTGRES_URL}"
|
||||
],
|
||||
"env": {
|
||||
"POSTGRES_URL": "${env:SQLALCHEMY_DATABASE_URI}"
|
||||
}
|
||||
},
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"."
|
||||
]
|
||||
},
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@angular/cli",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Project Architecture & Guidelines
|
||||
|
||||
## Monorepo Layout
|
||||
- **Backend**: `./barker` (Python, managed via `uv`)
|
||||
- **Frontend**: `./bookie` (Angular, managed via `npm`/`ng`)
|
||||
- **Environment**: Root `.env` file containing shared/database variables.
|
||||
|
||||
---
|
||||
|
||||
## Execution Rules & Commands
|
||||
|
||||
### Backend (`/barker`)
|
||||
- **Working Directory**: Always execute backend commands within `./barker` OR pass `--directory barker` to `uv`.
|
||||
- **Package & Script Management**:
|
||||
- Use `uv` exclusively. Never run bare `pip` or global `python`.
|
||||
- Run scripts/tests: `uv run python <script>` or `uv run pytest` from inside `./barker`.
|
||||
- Manage dependencies: `uv add <package>` or `uv remove <package>`.
|
||||
- **Environment**: The root `.env` file (`../.env` relative to `barker`) contains the database connection string and secret keys.
|
||||
|
||||
### Frontend (`/bookie`)
|
||||
- **Working Directory**: Always execute frontend commands within `./bookie`.
|
||||
- **Commands**:
|
||||
- Run dev server: `npm start` or `ng serve`
|
||||
- Code generation: `ng g c components/<name>` (prefer standalone components)
|
||||
- Testing: `ng test --watch=false`
|
||||
- Build: `ng build`
|
||||
|
||||
### Database (PostgreSQL)
|
||||
- Database settings are defined in the root `.env`.
|
||||
- Inspect schema, run migrations, or check types strictly via database tools or `uv run` scripts inside `./barker`.
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
trigger: glob
|
||||
globs: ./bookie/**/**
|
||||
---
|
||||
|
||||
# Persona
|
||||
|
||||
You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code.
|
||||
|
||||
## Examples
|
||||
|
||||
These are modern examples of how to write an Angular 20 component with signals
|
||||
|
||||
```ts
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: '{{tag-name}}-root',
|
||||
templateUrl: '{{tag-name}}.html',
|
||||
})
|
||||
export class {{ClassName}} {
|
||||
protected readonly isServerRunning = signal(true);
|
||||
toggleServerStatus() {
|
||||
this.isServerRunning.update(isServerRunning => !isServerRunning);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<section class="container">
|
||||
@if (isServerRunning()) {
|
||||
<span>Yes, the server is running</span>
|
||||
} @else {
|
||||
<span>No, the server is not running</span>
|
||||
}
|
||||
<button (click)="toggleServerStatus()">Toggle Server Status</button>
|
||||
</section>
|
||||
```
|
||||
|
||||
When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file.
|
||||
|
||||
## Resources
|
||||
|
||||
Here are the some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works
|
||||
https://angular.dev/essentials/components
|
||||
https://angular.dev/essentials/signals
|
||||
https://angular.dev/essentials/templates
|
||||
https://angular.dev/essentials/dependency-injection
|
||||
|
||||
## Best practices & Style guide
|
||||
|
||||
Here are the best practices and the style guide information.
|
||||
|
||||
### Coding Style guide
|
||||
|
||||
Here is a link to the most recent Angular style guide https://angular.dev/style-guide
|
||||
|
||||
### TypeScript Best Practices
|
||||
|
||||
- Use strict type checking
|
||||
- Prefer type inference when the type is obvious
|
||||
- Avoid the `any` type; use `unknown` when type is uncertain
|
||||
|
||||
### Angular Best Practices
|
||||
|
||||
- Always use standalone components over `NgModules`
|
||||
- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators
|
||||
- Use signals for state management
|
||||
- Implement lazy loading for feature routes
|
||||
- Use `NgOptimizedImage` for all static images.
|
||||
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
|
||||
|
||||
### Components
|
||||
|
||||
- Keep components small and focused on a single responsibility
|
||||
- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs
|
||||
- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs
|
||||
- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals.
|
||||
- Prefer inline templates for small components
|
||||
- Prefer Reactive forms instead of Template-driven ones
|
||||
- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
|
||||
- Do NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
|
||||
|
||||
### State Management
|
||||
|
||||
- Use signals for local component state
|
||||
- Use `computed()` for derived state
|
||||
- Keep state transformations pure and predictable
|
||||
- Do NOT use `mutate` on signals, use `update` or `set` instead
|
||||
|
||||
### Templates
|
||||
|
||||
- Keep templates simple and avoid complex logic
|
||||
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
|
||||
- Use the async pipe to handle observables
|
||||
- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes#
|
||||
|
||||
### Services
|
||||
|
||||
- Design services around a single responsibility
|
||||
- Use the `providedIn: 'root'` option for singleton services
|
||||
- Use the `inject()` function instead of constructor injection
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: angular-signals
|
||||
description: Writes modern Angular code using Standalone Components and Signals. Use whenever generating frontend UI or state logic.
|
||||
---
|
||||
# Modern Angular Standards
|
||||
|
||||
## Architecture
|
||||
- NEVER generate or use `NgModule`.
|
||||
- ALL components, directives, and pipes must be `standalone: true`.
|
||||
|
||||
## State Management (Signals)
|
||||
- Prefer Angular Signals (`signal()`, `computed()`, `effect()`) for synchronous, local component state instead of `BehaviorSubject`.
|
||||
- Use the new Signal-based inputs and outputs: `input()` and `output()` instead of `@Input()` and `@Output()`.
|
||||
- Use standard RxJS strictly for asynchronous streams (e.g., HTTP requests, router events).
|
||||
|
||||
## Control Flow
|
||||
- Use the new built-in control flow (`@if`, `@for`, `@switch`) instead of structural directives (`*ngIf`, `*ngFor`).
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: fastapi-pydantic-v2
|
||||
description: Writes modern FastAPI endpoints using Pydantic v2 syntax and Annotated dependencies. Use whenever generating or refactoring API routes.
|
||||
---
|
||||
# FastAPI & Pydantic v2 Standards
|
||||
|
||||
## Pydantic v2 Rules
|
||||
- NEVER use `.dict()` or `.json()`. ALWAYS use `.model_dump()` and `.model_dump_json()`.
|
||||
- NEVER use `@validator` or `@root_validator`. ALWAYS use `@field_validator` and `@model_validator(mode="before|after")`.
|
||||
- NEVER use `schema_extra`. Use `json_schema_extra` inside the `model_config` dict.
|
||||
- Use `pydantic_settings.BaseSettings` for all environment variable configurations.
|
||||
|
||||
## FastAPI Rules
|
||||
- ALWAYS use `typing.Annotated` for dependency injection (e.g., `db: Annotated[AsyncSession, Depends(get_db)]`).
|
||||
- Do not use `def` for endpoints that perform I/O; always use `async def`.
|
||||
- Raise `fastapi.HTTPException` for expected client errors instead of returning standard dictionaries.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: sqlalchemy-async
|
||||
description: Writes SQLAlchemy 2.0 async database queries using Psycopg3. Use whenever interacting with the PostgreSQL database.
|
||||
---
|
||||
# SQLAlchemy 2.0 Async Standards
|
||||
|
||||
## Model Definitions
|
||||
- ALWAYS use SQLAlchemy 2.0 Declarative Base with `Mapped` and `mapped_column`.
|
||||
- Example: `id: Mapped[int] = mapped_column(primary_key=True)`
|
||||
- NEVER use the old `Column(Integer, primary_key=True)` syntax.
|
||||
|
||||
## Querying
|
||||
- NEVER use `session.query()`. This is legacy 1.x syntax.
|
||||
- ALWAYS use `sqlalchemy.select()` combined with `await session.execute(stmt)`.
|
||||
- Extract results using `.scalars().all()` or `.scalar_one_or_none()`.
|
||||
|
||||
## Session Management
|
||||
- Use `sqlalchemy.ext.asyncio.AsyncSession`.
|
||||
- Ensure all database I/O is awaited.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: strict-mypy
|
||||
description: Enforces strict MyPy type checking conventions matching the project's pyproject.toml configuration. Use whenever writing or modifying Python code.
|
||||
---
|
||||
# Strict MyPy Typing Standards
|
||||
|
||||
This project uses a highly strict MyPy configuration (`strict = true`). When writing or refactoring Python code, you MUST adhere to the following rules to ensure the code passes type checking:
|
||||
|
||||
## 1. Absolute Signature Completeness
|
||||
- EVERY function and method must have fully typed arguments and a return type (`disallow_untyped_defs`, `disallow_incomplete_defs`).
|
||||
- ALWAYS specify `-> None` for functions and methods that do not return a value.
|
||||
- NEVER leave type hints partially defined (e.g., typing only some arguments).
|
||||
- Decorators must be completely type-hinted using `typing.Callable` or `typing.ParamSpec` (`disallow_untyped_decorators`).
|
||||
|
||||
## 2. No Implicit Optionals
|
||||
- `arg: str = None` is strictly forbidden.
|
||||
- You MUST explicitly union with None: `arg: str | None = None` (`no_implicit_optional`).
|
||||
|
||||
## 3. Strict Generics
|
||||
- NEVER use bare collections as types.
|
||||
- ALWAYS specify the inner types: use `list[str]`, `dict[str, int]`, or `tuple[int, ...]` instead of bare `list`, `dict`, or `tuple` (`disallow_any_generics`).
|
||||
|
||||
## 4. Restrictions on `Any` and Subclassing
|
||||
- Do not subclass from untyped external libraries or `Any` (`disallow_subclassing_any`).
|
||||
- Avoid using external types that have no stubs and resolve to `Any` (`disallow_any_unimported`).
|
||||
- Refrain from returning `Any` to avoid contaminating the type inference of calling functions (`warn_return_any`).
|
||||
|
||||
## 5. Pydantic Strictness
|
||||
- `pydantic.mypy` is active with strict initialization.
|
||||
- When instantiating Pydantic models, ensure you are passing exactly the expected typed arguments, as extra or untyped arguments will fail type checks (`init_typed = true`, `init_forbid_extra = true`).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Version control & IDEs
|
||||
.git
|
||||
.gitignore
|
||||
.vscode
|
||||
.idea
|
||||
.agents
|
||||
.gemini
|
||||
|
||||
# Build artifacts & compiled outputs
|
||||
frontend
|
||||
dist
|
||||
**/dist
|
||||
out-tsc
|
||||
**/out-tsc
|
||||
bazel-out
|
||||
|
||||
# Dependencies & framework caches
|
||||
node_modules
|
||||
**/node_modules
|
||||
.angular
|
||||
**/.angular
|
||||
.nx
|
||||
**/.nx
|
||||
.sass-cache
|
||||
**/.sass-cache
|
||||
|
||||
# Python virtual environments & caches
|
||||
.venv
|
||||
**/__pycache__
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
**/.pytest_cache
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info
|
||||
|
||||
# Testing & coverage
|
||||
coverage
|
||||
**/coverage
|
||||
.coverage
|
||||
htmlcov
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment & local deployment/dev files
|
||||
.env*
|
||||
!.env.example
|
||||
ansible
|
||||
deploy.sh
|
||||
lint.sh
|
||||
version_bump.sh
|
||||
Makefile
|
||||
README.md
|
||||
CHANGES.md
|
||||
typings
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -2,14 +2,15 @@ HOST=0.0.0.0
|
||||
PORT=9995
|
||||
LOG_LEVEL=INFO
|
||||
DEBUG=true
|
||||
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@localhost:5432/petty
|
||||
SQLALCHEMY_DATABASE_URI=postgresql+psycopg://postgres:123456@localhost:5432/petty
|
||||
MODULE_NAME=barker.main
|
||||
PROJECT_NAME=barker
|
||||
|
||||
MAX_WORKERS=4
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
NATS_URL=nats://localhost:4222
|
||||
SUBJECT="barker.print"
|
||||
STREAM="barker_print_stream"
|
||||
|
||||
# openssl rand -hex 32
|
||||
SECRET_KEY=611f9393e58f85521d16d9497bfd847ced70f2b99e12a9c6af40b7bd05cc7b1d
|
||||
@@ -24,6 +25,5 @@ TIMEZONE_OFFSET_MINUTES=330
|
||||
ALEMBIC_LOG_LEVEL=INFO
|
||||
ALEMBIC_SQLALCHEMY_LOG_LEVEL=WARN
|
||||
|
||||
WORKER_REDIS_HOST=
|
||||
WORKER_REDIS_PORT=6379
|
||||
WORKER_QUEUE_NAME=""
|
||||
|
||||
FASTAPI_ENV=development
|
||||
+50
-10
@@ -1,12 +1,52 @@
|
||||
*.pyc
|
||||
venv
|
||||
.project
|
||||
.pydevproject
|
||||
*/__pycache__/
|
||||
# IDEs & System Files
|
||||
.idea/
|
||||
*.egg-info/
|
||||
DB/Data/*.sql
|
||||
barker/static/
|
||||
build/
|
||||
dist/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build outputs
|
||||
frontend/
|
||||
dist/
|
||||
**/dist/
|
||||
out-tsc/
|
||||
**/out-tsc/
|
||||
bazel-out/
|
||||
|
||||
# Angular & Nx caches
|
||||
.angular/
|
||||
**/.angular/
|
||||
.nx/
|
||||
**/.nx/
|
||||
.sass-cache/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Python & Virtual Environments
|
||||
.venv/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
.mypy_cache/
|
||||
**/.mypy_cache/
|
||||
.ruff_cache/
|
||||
**/.ruff_cache/
|
||||
.pytest_cache/
|
||||
**/.pytest_cache/
|
||||
|
||||
# Testing & Coverage
|
||||
coverage/
|
||||
**/coverage/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Ignore Pyright-generated stubs
|
||||
*.pyi
|
||||
typings/
|
||||
@@ -0,0 +1 @@
|
||||
3.14
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": [
|
||||
"ms-vscode-remote.remote-wsl",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.debugpy",
|
||||
"charliermarsh.ruff",
|
||||
"docker.docker",
|
||||
"angular.ng-template",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"tombi-toml.tombi"
|
||||
]
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "Python: FastAPI",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "barker",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/barker",
|
||||
"justMyCode": true,
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
},
|
||||
{
|
||||
"name": "Python: FastAPI (Live Reload)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"barker.main:app",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"9995",
|
||||
"--reload"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/barker",
|
||||
"subProcess": true,
|
||||
"justMyCode": true,
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
// For more information, visit: https://angular.dev/ai/mcp
|
||||
"servers": {
|
||||
"angular-cli": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@angular/cli", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "barker/.venv/bin/python",
|
||||
"python.terminal.useEnvFile": true,
|
||||
"python.venvFolders": [
|
||||
"barker/.venv",
|
||||
"frank/.venv",
|
||||
".venv"
|
||||
],
|
||||
"python.venvPath": "${workspaceFolder}/barker",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
// Quality-of-life
|
||||
/* --- Pylance: keep IntelliSense, disable type checking --- */
|
||||
// "python.analysis.typeCheckingMode": "off",
|
||||
// "python.analysis.diagnosticMode": "openFilesOnly",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"python.analysis.extraPaths": [
|
||||
"${workspaceFolder}/barker",
|
||||
"./barker"
|
||||
],
|
||||
"python.autoComplete.extraPaths": [
|
||||
"${workspaceFolder}/barker",
|
||||
"./barker"
|
||||
],
|
||||
// Ruff (you have ruff installed via uv)
|
||||
"editor.formatOnSave": true,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"ruff.enable": true,
|
||||
"files.eol": "\n",
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"directory": "bookie",
|
||||
"changeProcessCWD": true
|
||||
}
|
||||
],
|
||||
"makefile.configureOnOpen": false,
|
||||
"files.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
".idea": true,
|
||||
"**/node_modules": true,
|
||||
"typings": true,
|
||||
"frontend": true,
|
||||
"**/.angular": true,
|
||||
"**/.nx": true,
|
||||
"**/out-tsc": true,
|
||||
"**/dist": true,
|
||||
"**/.sass-cache": true,
|
||||
"**/coverage": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/__pycache__": true,
|
||||
"**/.mypy_cache": true,
|
||||
"**/.ruff_cache": true,
|
||||
"**/.pytest_cache": true,
|
||||
".idea": true,
|
||||
"**/node_modules": true,
|
||||
"**/poetry.lock": true,
|
||||
"**/package-lock.json": true,
|
||||
"typings": true,
|
||||
"frontend/**": true,
|
||||
"**/.angular/**": true,
|
||||
"**/.nx/**": true,
|
||||
"**/out-tsc/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/.sass-cache/**": true,
|
||||
"**/coverage/**": true
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": "always",
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:venv",
|
||||
"python-envs.defaultPackageManager": "ms-python.python:pip",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/bookie"
|
||||
},
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/bookie"
|
||||
},
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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 bookie/package.json bookie/yarn.lock* bookie/package-lock.json* bookie/pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY /bookie ./
|
||||
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn run build; \
|
||||
elif [ -f package-lock.json ]; then npm run build; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM python:3.14-slim AS runner
|
||||
LABEL maintainer="Amritanshu <docker@tanshu.com>"
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# Compile bytecode
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# 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=barker/uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=barker/pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project
|
||||
|
||||
COPY /barker ./
|
||||
COPY --from=builder /frontend/browser /app/frontend
|
||||
|
||||
# Sync the project
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
EXPOSE 80
|
||||
|
||||
RUN chmod +x /app/docker-entrypoint.sh \
|
||||
&& ln -s /app/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
ENTRYPOINT ["docker-entrypoint.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", "barker.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--config", "./gunicorn.conf.py", "--log-config", "./logging.conf"]
|
||||
@@ -0,0 +1,28 @@
|
||||
.PHONY: build-production
|
||||
build-production: ## Build the production docker image.
|
||||
@docker buildx build \
|
||||
--platform linux/amd64,linux/arm64/v8 \
|
||||
--tag registry.tanshu.com/barker:latest \
|
||||
$(if $(TAG),--tag registry.tanshu.com/barker:$(TAG)) \
|
||||
--pull \
|
||||
--push \
|
||||
git@git.tanshu.com:tanshu/barker.git
|
||||
|
||||
.PHONY: build-check
|
||||
build-check: ## Multi-arch build without push (compile check)
|
||||
@docker buildx build \
|
||||
--platform linux/amd64,linux/arm64/v8 \
|
||||
--tag barker: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 barker:test \
|
||||
--pull \
|
||||
--progress=plain \
|
||||
--load \
|
||||
-
|
||||
@@ -1,41 +1,119 @@
|
||||
# Installation on Ubuntu 14.04
|
||||
## Prepare the system
|
||||
1. Install system-wide pip
|
||||
**Currently the ensurepip is broken in Python 3.4 / Ubuntu 14.04, therefore we need to install system-wide pip**
|
||||
```bash
|
||||
sudo apt-get install python3-pip
|
||||
# Installation linux
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
2. Install postgresql
|
||||
```bash
|
||||
sudo apt-get install postgresql postgresql-client pgadmin3
|
||||
```
|
||||
[Full Guide](https://help.ubuntu.com/community/PostgreSQL)
|
||||
Reload the shell:
|
||||
|
||||
3. Configure the server and set password
|
||||
```bash
|
||||
sudo -u postgres psql postgres
|
||||
\password postgres
|
||||
```zsh
|
||||
exec "$SHELL"
|
||||
```
|
||||
|
||||
4. Install components to build psycopg2
|
||||
```bash
|
||||
sudo apt-get install libpq-dev python3-dev
|
||||
Verify:
|
||||
|
||||
```zsh
|
||||
uv --version
|
||||
```
|
||||
|
||||
## Setup the virutal environment
|
||||
1. Create the virtual environment
|
||||
**Currently the ensurepip is broken in Python 3.4 / Ubuntu 14.04, therefore --without-pip is needed.**
|
||||
```bash
|
||||
pyvenv-3.4 env --without-pip --system-site-packages
|
||||
## 2. Install Python 3.14 using uv
|
||||
```zsh
|
||||
uv python install 3.14
|
||||
```
|
||||
|
||||
2. Activate the virtual environment
|
||||
```bash
|
||||
source env/bin/activate
|
||||
Set the local Python version for this project:
|
||||
|
||||
```zsh
|
||||
uv python pin 3.14
|
||||
```
|
||||
|
||||
3. Install pyramid, sqlachemy, psycopg2 and other dependencies:
|
||||
```bash
|
||||
python3 -m pip install pyramid waitress sqlalchemy zope.sqlalchemy psycopg2
|
||||
Verify:
|
||||
|
||||
```zsh
|
||||
python --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
File renamed without changes.
@@ -2,14 +2,15 @@ HOST=0.0.0.0
|
||||
PORT=80
|
||||
LOG_LEVEL=WARN
|
||||
DEBUG=false
|
||||
SQLALCHEMY_DATABASE_URI=postgresql://postgres:123456@db:5432/pettymhl
|
||||
SQLALCHEMY_DATABASE_URI=postgresql+psycopg://postgres:123456@postgres:5432/petty{{ app_name }}
|
||||
MODULE_NAME=barker.main
|
||||
PROJECT_NAME=barker
|
||||
MAX_WORKERS=4
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
SECRET_KEY=8b7f704ad1bbee3caa683a57440a6a371937fe5f64a7712a6a15bf3165c3c598
|
||||
MIDDLEWARE_SECRET_KEY=1aa5487223
|
||||
NATS_URL={{ nats_url }}
|
||||
SUBJECT=barker.print
|
||||
STREAM=barker_print_stream
|
||||
SECRET_KEY={{ secret_key }}
|
||||
MIDDLEWARE_SECRET_KEY={{ middleware_key }}
|
||||
ALGORITHM=HS256
|
||||
JWT_TOKEN_EXPIRE_MINUTES=30
|
||||
NEW_DAY_OFFSET_MINUTES=420
|
||||
@@ -0,0 +1,3 @@
|
||||
{{ host }} {
|
||||
reverse_proxy {{ host_directory }}:80
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# - Comments begin with the '#' character
|
||||
# - Blank lines are ignored
|
||||
# - Groups of hosts are delimited by [header] elements
|
||||
# - You can enter hostnames or ip addresses
|
||||
# - A hostname/ip can be a member of multiple groups
|
||||
|
||||
[barker]
|
||||
pkl ansible_host=clair var_file=vars/pkl.yml
|
||||
chd ansible_host=clair var_file=vars/chd.yml
|
||||
mhl ansible_host=clair var_file=vars/mhl.yml
|
||||
hin ansible_host=clair var_file=vars/hin.yml
|
||||
|
||||
[all:vars]
|
||||
ansible_python_interpreter=/usr/bin/python3
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#################################################
|
||||
# DO Community Playbooks: Docker
|
||||
#################################################
|
||||
---
|
||||
- name: Deploy Barker
|
||||
hosts: barker
|
||||
become: true
|
||||
vars_files:
|
||||
- vars/default.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Load per-host vars file from inventory (var_file)
|
||||
ansible.builtin.include_vars:
|
||||
file: "{{ var_file }}"
|
||||
when: var_file is defined
|
||||
|
||||
roles:
|
||||
- network
|
||||
- barker
|
||||
- caddy
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.docker
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
# Tasks for docker role
|
||||
- name: Log in to Docker registry
|
||||
community.docker.docker_login:
|
||||
registry: "{{ registry }}"
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
|
||||
- name: Pull Barker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ image_name }}"
|
||||
source: pull
|
||||
state: present
|
||||
force_source: true
|
||||
|
||||
- name: Ensure Host Directory exists
|
||||
ansible.builtin.file:
|
||||
path: "/var/lib/{{ host_directory }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Upload the .env file
|
||||
ansible.builtin.template:
|
||||
src: "files/.env"
|
||||
dest: "/var/lib/{{ host_directory }}/.env"
|
||||
mode: "0644"
|
||||
|
||||
- name: Create barker container
|
||||
community.docker.docker_container:
|
||||
name: "{{ host_directory }}"
|
||||
image: "{{ image_name }}"
|
||||
state: started
|
||||
restart_policy: "unless-stopped"
|
||||
env_file: "/var/lib/{{ host_directory }}/.env"
|
||||
networks:
|
||||
- name: "{{ docker_network }}"
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
caddy_container: caddy
|
||||
caddy_caddyfile_path: /var/lib/caddy/conf/Caddyfile
|
||||
# snippet_block: |
|
||||
# {{ host }} {
|
||||
# reverse_proxy {{ barker_container }}:80
|
||||
# }
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Reload Caddy configuration
|
||||
ansible.builtin.command: "docker exec -w /etc/caddy {{ caddy_container }} caddy reload"
|
||||
listen: "Reload Caddy"
|
||||
changed_when: true
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
- name: Read snippet from template file
|
||||
ansible.builtin.set_fact:
|
||||
caddy_snippet_block: "{{ lookup('template', 'files/Caddyfile.j2') }}"
|
||||
|
||||
- name: Read current Caddyfile
|
||||
ansible.builtin.slurp:
|
||||
path: "{{ caddy_caddyfile_path }}"
|
||||
register: caddy_caddyfile_raw
|
||||
|
||||
- name: Decode Caddyfile content
|
||||
ansible.builtin.set_fact:
|
||||
caddy_caddyfile_content: "{{ caddy_caddyfile_raw['content'] | b64decode }}"
|
||||
|
||||
- name: Check if snippet already exists
|
||||
ansible.builtin.set_fact:
|
||||
caddy_snippet_present: "{{ caddy_snippet_block in caddy_caddyfile_content }}"
|
||||
|
||||
- name: Add snippet if missing
|
||||
ansible.builtin.blockinfile:
|
||||
path: "{{ caddy_caddyfile_path }}"
|
||||
marker: "# {mark} Ansible managed Caddy snippet for {{ host }}"
|
||||
block: "{{ caddy_snippet_block }}"
|
||||
create: true
|
||||
mode: "0644"
|
||||
when: not caddy_snippet_present
|
||||
notify: Reload Caddy configuration
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Ensure 'barker' Docker network exists
|
||||
community.docker.docker_network:
|
||||
name: "{{ docker_network }}"
|
||||
state: present
|
||||
connected:
|
||||
- caddy
|
||||
- valkey
|
||||
- postgres
|
||||
appends: true
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
app_name: chd
|
||||
secret_key: d9e4facec94d7e0bf3d63ca03b1d78d834b158627b6593274f7fe27f6aed6db4
|
||||
middleware_key: 8d5f28b083
|
||||
|
||||
host: knox.greatbear.in
|
||||
host_directory: "barker-{{ app_name }}"
|
||||
docker_network: "{{ host_directory }}_net"
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# name: xxx
|
||||
# # openssl rand -hex 32
|
||||
# secret_key: 0000000000000000000000000000000000000000000000000000000000000000
|
||||
# # openssl rand -hex 5
|
||||
# middleware_key: 0000000000
|
||||
|
||||
registry: registry.tanshu.com
|
||||
username: ta-registry
|
||||
password: ff28a01f00c0f39315d94cd9dcb1e554968dba25676a8ea5f2be34e96a9a099f
|
||||
title: barker
|
||||
tag: latest
|
||||
|
||||
image_name: "{{ registry }}/barker:{{ tag }}"
|
||||
|
||||
# host_directory: "barker-{{ name }}"
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
app_name: hin
|
||||
secret_key: 029858927beda45d76317729824ae500d0938f04d4ca955f3afa95061c9a9298
|
||||
middleware_key: c4b0b98ff9
|
||||
|
||||
host: knox.mozimo.in
|
||||
host_directory: "barker-{{ app_name }}"
|
||||
docker_network: "{{ host_directory }}_net"
|
||||
nats_url: "nats://clair.lan.tanshu.com:4222"
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
app_name: mhl
|
||||
secret_key: 8b7f704ad1bbee3caa683a57440a6a371937fe5f64a7712a6a15bf3165c3c598
|
||||
middleware_key: 1aa5487223
|
||||
|
||||
host: knox.hngmohali.com
|
||||
host_directory: "barker-{{ app_name }}"
|
||||
docker_network: "{{ host_directory }}_net"
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
app_name: pkl
|
||||
secret_key: bd6e5dee0f3b8a6f0db50f7aa08e91d55b2ae5ab6df126defa37e80602481002
|
||||
middleware_key: 1d34ef6597
|
||||
|
||||
host: knox.hopsngrains.com
|
||||
host_directory: "barker-{{ app_name }}"
|
||||
docker_network: "{{ host_directory }}_net"
|
||||
@@ -1,3 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache
|
||||
@@ -1,4 +1,3 @@
|
||||
*.pyc
|
||||
*/__pycache__/
|
||||
*.egg-info/
|
||||
poetry.lock
|
||||
@@ -1,26 +1,20 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.2
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--config=barker/.flake8']
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
additional_dependencies: [toml]
|
||||
exclude: ^.*/?setup\.py$
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- --config=barker/pyproject.toml
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
|
||||
"python.interpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
|
||||
"python.venvFolders": [
|
||||
".venv"
|
||||
],
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"python.analysis.extraPaths": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"python.autoComplete.extraPaths": [
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": "always"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import logging
|
||||
|
||||
from sqlalchemy import create_engine, pool
|
||||
|
||||
from alembic import context
|
||||
from barker.core.config import settings
|
||||
from barker.db.base import User
|
||||
from sqlalchemy import create_engine, pool
|
||||
from barker.db.base import reg
|
||||
|
||||
|
||||
logging.basicConfig()
|
||||
@@ -14,7 +15,7 @@ logging.getLogger("alembic").setLevel(settings.ALEMBIC_LOG_LEVEL)
|
||||
# This line sets up loggers basically.
|
||||
|
||||
|
||||
target_metadata = User.metadata
|
||||
target_metadata = reg.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
|
||||
@@ -5,12 +5,14 @@ Revises: 3609c44430c8
|
||||
Create Date: 2021-10-22 09:36:11.746119
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import column, func, table, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0e326930b8a4"
|
||||
@@ -32,7 +34,7 @@ def upgrade():
|
||||
)
|
||||
|
||||
op.create_exclude_constraint(
|
||||
"uq_product_versions_product_id",
|
||||
op.f("uq_product_versions_product_id"),
|
||||
"product_versions",
|
||||
(prod.c.product_id, "="),
|
||||
(func.daterange(prod.c.valid_from, prod.c.valid_till, text("'[]'")), "&&"),
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Cancel Bill
|
||||
|
||||
Revision ID: 1d95eb10ea20
|
||||
Revises: 5967e6603c3e
|
||||
Create Date: 2023-03-17 01:04:25.809115
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy import column, table
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects.postgresql import ENUM
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1d95eb10ea20"
|
||||
down_revision = "5967e6603c3e"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
# op.execute("ALTER TYPE voucher_type ADD VALUE 'CANCEL';")
|
||||
reporting_level = sa.Enum("Skip", "Aggregate", "Detailed", name="reportinglevel")
|
||||
reporting_level.create(op.get_bind())
|
||||
voucher_type = sa.Enum("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", "CANCEL", name="vouchertype")
|
||||
voucher_type.create(op.get_bind())
|
||||
|
||||
op.execute("ALTER TABLE settle_options ALTER COLUMN voucher_type DROP DEFAULT;")
|
||||
op.execute("ALTER TABLE settle_options ALTER COLUMN reporting_level DROP DEFAULT;")
|
||||
op.execute(
|
||||
"ALTER TABLE settle_options ALTER COLUMN voucher_type TYPE vouchertype USING voucher_type::text::vouchertype;"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE settle_options "
|
||||
+ "ALTER COLUMN reporting_level "
|
||||
+ "TYPE reportinglevel USING reporting_level::text::reportinglevel;"
|
||||
)
|
||||
op.execute("ALTER TABLE vouchers ALTER COLUMN voucher_type DROP DEFAULT;")
|
||||
op.execute("ALTER TABLE vouchers ALTER COLUMN voucher_type TYPE vouchertype USING voucher_type::text::vouchertype;")
|
||||
|
||||
op.execute("DROP TYPE voucher_type;")
|
||||
op.execute("DROP TYPE reporting_level;")
|
||||
op.create_index(op.f("ix_vouchers_date"), "vouchers", ["date"], unique=False)
|
||||
|
||||
settle_options()
|
||||
regimes()
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def settle_options():
|
||||
reporting_level = ENUM("Skip", "Aggregate", "Detailed", name="reporting_level", create_type=False)
|
||||
voucher_type = ENUM(
|
||||
"KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", "CANCEL", name="voucher_type", create_type=False
|
||||
)
|
||||
so = table(
|
||||
"settle_options",
|
||||
column("id", sa.Integer()),
|
||||
column("name", sa.Unicode(length=255)),
|
||||
column("has_reason", sa.Boolean()),
|
||||
column("reporting_level", reporting_level),
|
||||
column("voucher_type", voucher_type),
|
||||
column("is_fixture", sa.Boolean()),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
so.insert().values(
|
||||
id=12,
|
||||
name="Cancelled",
|
||||
has_reason=True,
|
||||
reporting_level="Detailed",
|
||||
voucher_type="CANCEL",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def regimes():
|
||||
re = sa.table(
|
||||
"regimes",
|
||||
sa.column("id", sa.Integer()),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
sa.column("header", sa.Unicode(length=255)),
|
||||
sa.column("prefix", sa.Unicode(length=255)),
|
||||
sa.column("is_fixture", sa.Boolean()),
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=12,
|
||||
name="Cancelled",
|
||||
header="",
|
||||
prefix="C",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_vouchers_date"), table_name="vouchers")
|
||||
op.alter_column(
|
||||
"vouchers",
|
||||
"voucher_type",
|
||||
existing_type=sa.Enum("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", "CANCEL", name="vouchertype"),
|
||||
type_=postgresql.ENUM("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", name="voucher_type"),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'KOT'::voucher_type"),
|
||||
)
|
||||
op.alter_column(
|
||||
"settle_options",
|
||||
"reporting_level",
|
||||
existing_type=sa.Enum("Skip", "Aggregate", "Detailed", name="reportinglevel"),
|
||||
type_=postgresql.ENUM("Skip", "Aggregate", "Detailed", name="reporting_level"),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'Skip'::reporting_level"),
|
||||
)
|
||||
op.alter_column(
|
||||
"settle_options",
|
||||
"voucher_type",
|
||||
existing_type=sa.Enum("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", "CANCEL", name="vouchertype"),
|
||||
type_=postgresql.ENUM("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", name="voucher_type"),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'KOT'::voucher_type"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,291 @@
|
||||
"""combine skus
|
||||
|
||||
Revision ID: 1e0daf6bc1ae
|
||||
Revises: c116517977af
|
||||
Create Date: 2026-01-23 15:56:01.354574
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1e0daf6bc1ae"
|
||||
down_revision = "c116517977af"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
-- Merge contiguous identical sku_versions (same business fields) into single rows.
|
||||
|
||||
-- 0) Work table: compute vf/vt sentinels + run_id (islands)
|
||||
CREATE TEMP TABLE tmp_sku_work ON COMMIT DROP AS
|
||||
WITH base AS (
|
||||
SELECT
|
||||
sv.id,
|
||||
sv.sku_id,
|
||||
sv.units,
|
||||
sv.fraction,
|
||||
sv.product_yield,
|
||||
sv.cost_price,
|
||||
sv.sale_price,
|
||||
sv.has_happy_hour,
|
||||
sv.menu_category_id,
|
||||
sv.valid_from,
|
||||
sv.valid_till,
|
||||
COALESCE(sv.valid_from, DATE '1900-01-01') AS vf,
|
||||
COALESCE(sv.valid_till, DATE '9999-12-31') AS vt
|
||||
FROM sku_versions sv
|
||||
),
|
||||
ordered AS (
|
||||
SELECT
|
||||
b.*,
|
||||
LAG(vt) OVER (
|
||||
PARTITION BY sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id
|
||||
ORDER BY vf, vt, id
|
||||
) AS prev_vt
|
||||
FROM base b
|
||||
),
|
||||
flagged AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE
|
||||
WHEN o.prev_vt IS NULL THEN 1
|
||||
WHEN o.vf = (o.prev_vt + INTERVAL '1 day')::date THEN 0
|
||||
ELSE 1
|
||||
END AS is_new_run
|
||||
FROM ordered o
|
||||
),
|
||||
runs AS (
|
||||
SELECT
|
||||
f.*,
|
||||
SUM(is_new_run) OVER (
|
||||
PARTITION BY sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id
|
||||
ORDER BY vf, vt, id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS run_id
|
||||
FROM flagged f
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id,
|
||||
valid_from, valid_till,
|
||||
vf, vt,
|
||||
run_id
|
||||
FROM runs;
|
||||
|
||||
-- 1) Survivors + merged validity (only runs with >1 row)
|
||||
CREATE TEMP TABLE tmp_sku_merge ON COMMIT DROP AS
|
||||
WITH run_stats AS (
|
||||
SELECT
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id,
|
||||
run_id,
|
||||
MIN(vf) AS min_vf,
|
||||
MAX(vt) AS max_vt,
|
||||
COUNT(*) AS cnt
|
||||
FROM tmp_sku_work
|
||||
GROUP BY
|
||||
sku_id, units, fraction, product_yield, cost_price, sale_price, has_happy_hour, menu_category_id, run_id
|
||||
HAVING COUNT(*) > 1
|
||||
),
|
||||
survivors AS (
|
||||
SELECT DISTINCT ON (
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id
|
||||
)
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id,
|
||||
w.id AS survivor_id
|
||||
FROM tmp_sku_work w
|
||||
JOIN run_stats s
|
||||
ON s.sku_id = w.sku_id
|
||||
AND s.units = w.units
|
||||
AND s.fraction = w.fraction
|
||||
AND s.product_yield = w.product_yield
|
||||
AND s.cost_price = w.cost_price
|
||||
AND s.sale_price = w.sale_price
|
||||
AND s.has_happy_hour = w.has_happy_hour
|
||||
AND s.menu_category_id = w.menu_category_id
|
||||
AND s.run_id = w.run_id
|
||||
ORDER BY
|
||||
w.sku_id, w.units, w.fraction, w.product_yield, w.cost_price, w.sale_price, w.has_happy_hour, w.menu_category_id, w.run_id,
|
||||
w.vf, w.vt, w.id
|
||||
)
|
||||
SELECT
|
||||
sv.survivor_id,
|
||||
sv.sku_id, sv.units, sv.fraction, sv.product_yield, sv.cost_price, sv.sale_price, sv.has_happy_hour, sv.menu_category_id, sv.run_id,
|
||||
CASE WHEN s.min_vf = DATE '1900-01-01' THEN NULL ELSE s.min_vf END AS new_valid_from,
|
||||
CASE WHEN s.max_vt = DATE '9999-12-31' THEN NULL ELSE s.max_vt END AS new_valid_till
|
||||
FROM survivors sv
|
||||
JOIN run_stats s
|
||||
ON s.sku_id = sv.sku_id
|
||||
AND s.units = sv.units
|
||||
AND s.fraction = sv.fraction
|
||||
AND s.product_yield = sv.product_yield
|
||||
AND s.cost_price = sv.cost_price
|
||||
AND s.sale_price = sv.sale_price
|
||||
AND s.has_happy_hour = sv.has_happy_hour
|
||||
AND s.menu_category_id = sv.menu_category_id
|
||||
AND s.run_id = sv.run_id;
|
||||
|
||||
-- 2) Rows to delete = everything in a merged run except the survivor
|
||||
CREATE TEMP TABLE tmp_sku_delete ON COMMIT DROP AS
|
||||
SELECT w.id
|
||||
FROM tmp_sku_work w
|
||||
JOIN tmp_sku_merge m
|
||||
ON m.sku_id = w.sku_id
|
||||
AND m.units = w.units
|
||||
AND m.fraction = w.fraction
|
||||
AND m.product_yield = w.product_yield
|
||||
AND m.cost_price = w.cost_price
|
||||
AND m.sale_price = w.sale_price
|
||||
AND m.has_happy_hour = w.has_happy_hour
|
||||
AND m.menu_category_id = w.menu_category_id
|
||||
AND m.run_id = w.run_id
|
||||
WHERE w.id <> m.survivor_id;
|
||||
|
||||
-- 3) Delete first (avoid exclusion overlap), then update survivor validity
|
||||
DELETE FROM sku_versions
|
||||
WHERE id IN (SELECT id FROM tmp_sku_delete);
|
||||
|
||||
UPDATE sku_versions sv
|
||||
SET valid_from = m.new_valid_from,
|
||||
valid_till = m.new_valid_till
|
||||
FROM tmp_sku_merge m
|
||||
WHERE sv.id = m.survivor_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
-- Merge contiguous identical product_versions (ignoring fraction_units)
|
||||
|
||||
-- 0) Work table: compute vf/vt sentinels + run_id (islands)
|
||||
CREATE TEMP TABLE tmp_pv_work ON COMMIT DROP AS
|
||||
WITH base AS (
|
||||
SELECT
|
||||
pv.id,
|
||||
pv.product_id,
|
||||
pv.name,
|
||||
pv.valid_from,
|
||||
pv.valid_till,
|
||||
COALESCE(pv.valid_from, DATE '1900-01-01') AS vf,
|
||||
COALESCE(pv.valid_till, DATE '9999-12-31') AS vt
|
||||
FROM product_versions pv
|
||||
),
|
||||
ordered AS (
|
||||
SELECT
|
||||
b.*,
|
||||
LAG(vt) OVER (
|
||||
PARTITION BY product_id, name
|
||||
ORDER BY vf, vt, id
|
||||
) AS prev_vt
|
||||
FROM base b
|
||||
),
|
||||
flagged AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE
|
||||
WHEN o.prev_vt IS NULL THEN 1
|
||||
WHEN o.vf = (o.prev_vt + INTERVAL '1 day')::date THEN 0
|
||||
ELSE 1
|
||||
END AS is_new_run
|
||||
FROM ordered o
|
||||
),
|
||||
runs AS (
|
||||
SELECT
|
||||
f.*,
|
||||
SUM(is_new_run) OVER (
|
||||
PARTITION BY product_id, name
|
||||
ORDER BY vf, vt, id
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS run_id
|
||||
FROM flagged f
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
product_id,
|
||||
name,
|
||||
valid_from,
|
||||
valid_till,
|
||||
vf, vt,
|
||||
run_id
|
||||
FROM runs;
|
||||
|
||||
-- 1) Survivors + merged validity (only runs with >1 row)
|
||||
CREATE TEMP TABLE tmp_pv_merge ON COMMIT DROP AS
|
||||
WITH run_stats AS (
|
||||
SELECT
|
||||
product_id,
|
||||
name,
|
||||
run_id,
|
||||
MIN(vf) AS min_vf,
|
||||
MAX(vt) AS max_vt,
|
||||
COUNT(*) AS cnt
|
||||
FROM tmp_pv_work
|
||||
GROUP BY product_id, name, run_id
|
||||
HAVING COUNT(*) > 1
|
||||
),
|
||||
survivors AS (
|
||||
-- deterministic survivor: earliest vf, then earliest vt, then lowest id
|
||||
SELECT DISTINCT ON (w.product_id, w.name, w.run_id)
|
||||
w.product_id,
|
||||
w.name,
|
||||
w.run_id,
|
||||
w.id AS survivor_id
|
||||
FROM tmp_pv_work w
|
||||
JOIN run_stats s
|
||||
ON s.product_id = w.product_id
|
||||
AND s.name = w.name
|
||||
AND s.run_id = w.run_id
|
||||
ORDER BY w.product_id, w.name, w.run_id, w.vf, w.vt, w.id
|
||||
)
|
||||
SELECT
|
||||
sv.survivor_id,
|
||||
sv.product_id,
|
||||
sv.name,
|
||||
sv.run_id,
|
||||
CASE WHEN s.min_vf = DATE '1900-01-01' THEN NULL ELSE s.min_vf END AS new_valid_from,
|
||||
CASE WHEN s.max_vt = DATE '9999-12-31' THEN NULL ELSE s.max_vt END AS new_valid_till
|
||||
FROM survivors sv
|
||||
JOIN run_stats s
|
||||
ON s.product_id = sv.product_id
|
||||
AND s.name = sv.name
|
||||
AND s.run_id = sv.run_id;
|
||||
|
||||
-- 2) Rows to delete = everything in a merged run except the survivor
|
||||
CREATE TEMP TABLE tmp_pv_delete ON COMMIT DROP AS
|
||||
SELECT w.id
|
||||
FROM tmp_pv_work w
|
||||
JOIN tmp_pv_merge m
|
||||
ON m.product_id = w.product_id
|
||||
AND m.name = w.name
|
||||
AND m.run_id = w.run_id
|
||||
WHERE w.id <> m.survivor_id;
|
||||
|
||||
-- 3) Delete first (avoid exclusion overlap), then update survivor validity
|
||||
DELETE FROM product_versions
|
||||
WHERE id IN (SELECT id FROM tmp_pv_delete);
|
||||
|
||||
UPDATE product_versions pv
|
||||
SET valid_from = m.new_valid_from,
|
||||
valid_till = m.new_valid_till
|
||||
FROM tmp_pv_merge m
|
||||
WHERE pv.id = m.survivor_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Compatibility updates for GuestBookType enum
|
||||
op.execute("ALTER TYPE guestbooktype ADD VALUE IF NOT EXISTS 'running'")
|
||||
op.execute("ALTER TYPE guestbooktype ADD VALUE IF NOT EXISTS 'printed'")
|
||||
op.execute("ALTER TYPE guestbooktype ADD VALUE IF NOT EXISTS 'old'")
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -0,0 +1,236 @@
|
||||
"""add stock_keeping_units + sku_versions"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "32c508eed4df"
|
||||
down_revision = "7e9944b430d6"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ---------------------------------------------------------------------
|
||||
# 0) Add NON-VERSIONED sort_order columns (but do NOT remove versioned ones here)
|
||||
# ---------------------------------------------------------------------
|
||||
op.add_column(
|
||||
"products",
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) stock_keeping_units
|
||||
# ---------------------------------------------------------------------
|
||||
op.create_table(
|
||||
"stock_keeping_units",
|
||||
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("product_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("is_not_available", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["product_id"], ["products.id"], name=op.f("fk_stock_keeping_units_product_id_products")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_stock_keeping_units")),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 2) sku_versions
|
||||
# ---------------------------------------------------------------------
|
||||
op.create_table(
|
||||
"sku_versions",
|
||||
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("sku_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("units", sa.Unicode(), nullable=False),
|
||||
sa.Column("fraction", sa.Numeric(precision=15, scale=5), nullable=False),
|
||||
sa.Column("product_yield", sa.Numeric(precision=15, scale=5), nullable=False),
|
||||
sa.Column("cost_price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("sale_price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("has_happy_hour", sa.Boolean(), nullable=False),
|
||||
sa.Column("menu_category_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("valid_from", sa.Date(), nullable=True),
|
||||
sa.Column("valid_till", sa.Date(), nullable=True),
|
||||
postgresql.ExcludeConstraint(
|
||||
(sa.column("sku_id"), "="),
|
||||
(sa.text("daterange(valid_from, valid_till, '[]')"), "&&"),
|
||||
using="gist",
|
||||
name=op.f("uq_sku_versions_sku_id"),
|
||||
),
|
||||
postgresql.ExcludeConstraint(
|
||||
(sa.column("sku_id"), "="),
|
||||
(sa.column("units"), "="),
|
||||
(sa.text("daterange(valid_from, valid_till, '[]')"), "&&"),
|
||||
using="gist",
|
||||
name=op.f("uq_sku_versions_sku_id_units"),
|
||||
),
|
||||
# postgresql.ExcludeConstraint((sa.column('units'), '='), (sa.text("daterange(valid_from, valid_till, '[]')"), '&&'), using='gist', name=op.f('uq_sku_versions_units')),
|
||||
sa.ForeignKeyConstraint(
|
||||
["menu_category_id"], ["menu_categories.id"], name=op.f("fk_sku_versions_menu_category_id_menu_categories")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["sku_id"], ["stock_keeping_units.id"], name=op.f("fk_sku_versions_sku_id_stock_keeping_units")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_sku_versions")),
|
||||
sa.CheckConstraint("length(trim(units)) >= 1", name="units_not_blank"),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 3) Data migration
|
||||
# - Create exactly 1 SKU per product.
|
||||
# - Migrate existing product_versions rows into sku_versions rows.
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# 3a) Insert a SKU for every product
|
||||
products = sa.table("products", sa.column("id", postgresql.UUID(as_uuid=True)))
|
||||
|
||||
stock_keeping_units = sa.table(
|
||||
"stock_keeping_units",
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("product_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("is_not_available", sa.Boolean()),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
stock_keeping_units.insert().from_select(
|
||||
["id", "product_id", "is_not_available", "sort_order"],
|
||||
sa.select(
|
||||
sa.func.gen_random_uuid(),
|
||||
products.c.id,
|
||||
sa.false(),
|
||||
sa.literal(0),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# 3b) Migrate product_versions -> sku_versions, mapping by product_id to the single SKU
|
||||
# NOTE: this assumes product_versions still has the legacy SKU-ish columns.
|
||||
pv = sa.table(
|
||||
"product_versions",
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("product_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("units", sa.Unicode()),
|
||||
sa.column("price", sa.Numeric(15, 2)),
|
||||
sa.column("quantity", sa.Numeric(15, 2)),
|
||||
sa.column("has_happy_hour", sa.Boolean()),
|
||||
sa.column("is_not_available", sa.Boolean()),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
sa.column("menu_category_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("valid_from", sa.Date()),
|
||||
sa.column("valid_till", sa.Date()),
|
||||
)
|
||||
|
||||
# Insert one sku_versions row per product_versions row
|
||||
sku_versions = sa.table(
|
||||
"sku_versions",
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("sku_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("units", sa.Unicode()),
|
||||
sa.column("fraction", sa.Numeric(15, 5)),
|
||||
sa.column("product_yield", sa.Numeric(15, 5)),
|
||||
sa.column("cost_price", sa.Numeric(15, 2)),
|
||||
sa.column("sale_price", sa.Numeric(15, 2)),
|
||||
sa.column("has_happy_hour", sa.Boolean()),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
sa.column("menu_category_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("valid_from", sa.Date()),
|
||||
sa.column("valid_till", sa.Date()),
|
||||
)
|
||||
|
||||
# Join: product_versions.product_id -> stock_keeping_units.product_id (single SKU per product)
|
||||
# Mapping rules (based on old vs new models you showed):
|
||||
# - units -> units
|
||||
# - fraction -> 1.0 (since old PV had quantity/units rather than fraction)
|
||||
# - product_yield -> quantity (old PV.quantity)
|
||||
# - cost_price -> 0.0 (not present in old PV)
|
||||
# - sale_price -> price (old PV.price)
|
||||
# - has_happy_hour -> has_happy_hour
|
||||
# - sort_order -> sort_order
|
||||
# - menu_category_id -> menu_category_id
|
||||
# - valid_from/till -> valid_from/till
|
||||
#
|
||||
# Also propagate is_not_available to the SKU row:
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE stock_keeping_units sku
|
||||
SET is_not_available = pv.is_not_available,
|
||||
sort_order = pv.sort_order
|
||||
FROM product_versions pv
|
||||
WHERE pv.product_id = sku.product_id
|
||||
AND pv.valid_till IS NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.execute(
|
||||
sku_versions.insert().from_select(
|
||||
[
|
||||
"id",
|
||||
"sku_id",
|
||||
"units",
|
||||
"fraction",
|
||||
"product_yield",
|
||||
"cost_price",
|
||||
"sale_price",
|
||||
"has_happy_hour",
|
||||
"menu_category_id",
|
||||
"valid_from",
|
||||
"valid_till",
|
||||
],
|
||||
sa.select(
|
||||
sa.func.gen_random_uuid(),
|
||||
stock_keeping_units.c.id,
|
||||
sa.func.coalesce(sa.func.nullif(sa.func.btrim(pv.c.units), ""), sa.literal("por")),
|
||||
sa.cast(1.0, sa.Numeric(15, 5)),
|
||||
sa.cast(1.0, sa.Numeric(15, 5)),
|
||||
sa.cast(0.0, sa.Numeric(15, 2)),
|
||||
sa.cast(pv.c.price, sa.Numeric(15, 2)),
|
||||
pv.c.has_happy_hour,
|
||||
pv.c.menu_category_id,
|
||||
pv.c.valid_from,
|
||||
pv.c.valid_till,
|
||||
).select_from(pv.join(stock_keeping_units, stock_keeping_units.c.product_id == pv.c.product_id)),
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 4) Update foreign keys from product_versions to sku_version
|
||||
# ---------------------------------------------------------------------
|
||||
op.drop_constraint(op.f("fk_inventories_product_id_products"), "inventories", type_="foreignkey")
|
||||
op.alter_column("inventories", "product_id", new_column_name="sku_id")
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE inventories i
|
||||
SET sku_id = sku.id
|
||||
FROM stock_keeping_units sku
|
||||
WHERE i.sku_id = sku.product_id
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.create_foreign_key(
|
||||
op.f("fk_inventories_sku_id_stock_keeping_units"), "inventories", "stock_keeping_units", ["sku_id"], ["id"]
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 5) Drop old columns from product_versions
|
||||
# ---------------------------------------------------------------------
|
||||
op.alter_column("product_versions", "units", new_column_name="fraction_units")
|
||||
op.drop_constraint(op.f("fk_products_menu_category_id_menu_categories"), "product_versions", type_="foreignkey")
|
||||
op.create_check_constraint(
|
||||
"name_not_blank",
|
||||
"product_versions",
|
||||
"length(trim(name)) >= 1",
|
||||
)
|
||||
for col in ["price", "quantity", "has_happy_hour", "is_not_available", "menu_category_id", "sort_order"]:
|
||||
op.drop_column("product_versions", col)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# You can implement if you need it; left explicit to avoid accidental data loss assumptions.
|
||||
raise NotImplementedError("Downgrade not implemented")
|
||||
@@ -5,17 +5,19 @@ Revises: 8c06ac60d125
|
||||
Create Date: 2020-10-27 19:38:48.445908
|
||||
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
from sqlalchemy import column, func, select, table, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "34fe3d061c5f"
|
||||
down_revision = "8c06ac60d125"
|
||||
@@ -181,7 +183,7 @@ def upgrade():
|
||||
op.drop_constraint("pk_products", "products", type_="primary")
|
||||
op.rename_table("products", "product_versions")
|
||||
with op.batch_alter_table("product_versions") as batch_op:
|
||||
batch_op.add_column(sa.Column("product_id", postgresql.UUID(as_uuid=True), nullable=True)),
|
||||
(batch_op.add_column(sa.Column("product_id", postgresql.UUID(as_uuid=True), nullable=True)),)
|
||||
batch_op.add_column(sa.Column("valid_from", sa.Date(), nullable=True))
|
||||
batch_op.add_column(sa.Column("valid_till", sa.Date(), nullable=True))
|
||||
batch_op.create_primary_key("pk_product_versions", ["id"])
|
||||
|
||||
@@ -5,6 +5,7 @@ Revises: 81d94c5223a7
|
||||
Create Date: 2021-09-28 09:18:10.666701
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""bundles
|
||||
|
||||
Revision ID: 367ecf7b898f
|
||||
Revises: 1e0daf6bc1ae
|
||||
Create Date: 2026-01-28 16:11:21.874409
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "367ecf7b898f"
|
||||
down_revision = "1e0daf6bc1ae"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"bundle_items",
|
||||
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("bundle_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("item_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("quantity", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("sale_price", sa.Numeric(precision=15, scale=2), nullable=False),
|
||||
sa.Column("print_in_bill", sa.Boolean(), server_default=sa.text("true"), nullable=False),
|
||||
sa.Column("valid_from", sa.Date(), nullable=True),
|
||||
sa.Column("valid_till", sa.Date(), nullable=True),
|
||||
postgresql.ExcludeConstraint(
|
||||
(sa.column("bundle_id"), "="),
|
||||
(sa.column("item_id"), "="),
|
||||
(sa.text("daterange(valid_from, valid_till, '[]')"), "&&"),
|
||||
using="gist",
|
||||
name=op.f("uq_bundle_items_bundle_id_item_id"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["bundle_id"], ["stock_keeping_units.id"], name=op.f("fk_bundle_items_bundle_id_stock_keeping_units")
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["item_id"], ["stock_keeping_units.id"], name=op.f("fk_bundle_items_item_id_stock_keeping_units")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_bundle_items")),
|
||||
)
|
||||
|
||||
inventory_type = sa.Enum("regular", "bundle", "bundle_item", name="inventorytype")
|
||||
inventory_type.create(op.get_bind())
|
||||
op.add_column("inventories", sa.Column("type", inventory_type, server_default=sa.text("'regular'"), nullable=False))
|
||||
op.add_column("inventories", sa.Column("parent_id", sa.Uuid(), nullable=True))
|
||||
op.drop_constraint(op.f("uq_inventories_kot_id"), "inventories", type_="unique")
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_inventories_kot_id"),
|
||||
"inventories",
|
||||
["kot_id", "sku_id", "is_happy_hour", "type", "parent_id"],
|
||||
postgresql_nulls_not_distinct=True,
|
||||
)
|
||||
op.create_foreign_key(
|
||||
op.f("fk_inventories_parent_id_inventories"), "inventories", "inventories", ["parent_id"], ["id"]
|
||||
)
|
||||
op.add_column(
|
||||
"stock_keeping_units", sa.Column("is_bundle", sa.Boolean(), server_default=sa.text("false"), nullable=False)
|
||||
)
|
||||
mc = sa.table(
|
||||
"menu_categories",
|
||||
sa.column("id", sa.Uuid()),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
sa.column("is_active", sa.Boolean()),
|
||||
sa.column("is_fixture", sa.Boolean()),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
mc.insert().values(
|
||||
id="6752ed9d-6f1a-4941-940a-17759dcc6720",
|
||||
name="Bundles",
|
||||
is_active=True,
|
||||
is_fixture=True,
|
||||
sort_order=0,
|
||||
)
|
||||
)
|
||||
|
||||
customers = sa.table(
|
||||
"customers",
|
||||
sa.column("id", sa.Uuid),
|
||||
sa.column("name", sa.Text),
|
||||
sa.column("phone", sa.Text),
|
||||
)
|
||||
|
||||
guest_book = sa.table(
|
||||
"guest_book",
|
||||
sa.column("customer_id", sa.Uuid),
|
||||
)
|
||||
|
||||
voucher = sa.table(
|
||||
"vouchers",
|
||||
sa.column("customer_id", sa.Uuid),
|
||||
)
|
||||
|
||||
bad_customer_ids = sa.select(customers.c.id).where(
|
||||
sa.func.length(sa.func.trim(customers.c.phone)) == 0,
|
||||
)
|
||||
|
||||
op.execute(guest_book.delete().where(guest_book.c.customer_id.in_(bad_customer_ids)))
|
||||
|
||||
op.execute(voucher.update().where(voucher.c.customer_id.in_(bad_customer_ids)).values(customer_id=None))
|
||||
|
||||
op.execute(customers.delete().where(customers.c.id.in_(bad_customer_ids)))
|
||||
|
||||
op.execute(
|
||||
customers.update()
|
||||
.where(
|
||||
sa.func.length(sa.func.trim(customers.c.name)) == 0,
|
||||
)
|
||||
.values(name=customers.c.phone)
|
||||
)
|
||||
|
||||
with op.batch_alter_table("customers") as batch_op:
|
||||
batch_op.create_check_constraint("name_not_blank", sa.text("length(trim(both from name)) >= 1"))
|
||||
batch_op.create_check_constraint("phone_not_blank", sa.text("length(trim(both from phone)) >= 1"))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("bundle_items")
|
||||
op.drop_column("stock_keeping_units", "is_bundle")
|
||||
op.drop_constraint(op.f("fk_inventories_parent_id_inventories"), "inventories", type_="foreignkey")
|
||||
op.drop_constraint(op.f("uq_inventories_kot_id"), "inventories", type_="unique")
|
||||
op.create_unique_constraint(
|
||||
op.f("uq_inventories_kot_id"),
|
||||
"inventories",
|
||||
["kot_id", "sku_id", "is_happy_hour", "price"],
|
||||
postgresql_nulls_not_distinct=False,
|
||||
)
|
||||
op.drop_column("inventories", "parent_id")
|
||||
op.drop_column("inventories", "type")
|
||||
mc = sa.table(
|
||||
"menu_categories",
|
||||
sa.column("id", sa.Uuid()),
|
||||
)
|
||||
op.execute(mc.delete().where(mc.c.id == "6752ed9d-6f1a-4941-940a-17759dcc6720"))
|
||||
|
||||
with op.batch_alter_table("customers") as batch_op:
|
||||
batch_op.drop_constraint("name_not_blank", type_="check")
|
||||
batch_op.drop_constraint("phone_not_blank", type_="check")
|
||||
@@ -5,14 +5,16 @@ Revises: 34fe3d061c5f
|
||||
Create Date: 2020-12-10 08:29:39.175097
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
from sqlalchemy import column, table
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "48bc1c7c07ce"
|
||||
down_revision = "34fe3d061c5f"
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"""tax regimes
|
||||
|
||||
Revision ID: 5967e6603c3e
|
||||
Revises: f135019a4ebf
|
||||
Create Date: 2023-02-22 03:55:58.764092
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.dialects.postgresql import ENUM
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5967e6603c3e"
|
||||
down_revision = "f135019a4ebf"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
create_regime_table()
|
||||
create_bills_table()
|
||||
update_taxes()
|
||||
op.drop_index("ix_vouchers_date", table_name="vouchers")
|
||||
op.drop_constraint("uq_vouchers_bill_id", "vouchers", type_="unique")
|
||||
op.drop_column("vouchers", "bill_id")
|
||||
p = sa.table(
|
||||
"permissions",
|
||||
sa.column("id", postgresql.UUID(as_uuid=True)),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
)
|
||||
op.execute(p.insert().values(id="28e2194d-b830-4282-b8c5-3b8522b48894", name="Regimes"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def create_regime_table():
|
||||
op.create_table(
|
||||
"regimes",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.Unicode(length=255), nullable=False),
|
||||
sa.Column("header", sa.Unicode(length=255), nullable=False),
|
||||
sa.Column("prefix", sa.Unicode(length=255), nullable=False),
|
||||
sa.Column("is_fixture", sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_regimes")),
|
||||
sa.UniqueConstraint("name", name=op.f("uq_regimes_name")),
|
||||
sa.UniqueConstraint("prefix", name=op.f("uq_regimes_prefix")),
|
||||
)
|
||||
re = sa.table(
|
||||
"regimes",
|
||||
sa.column("id", sa.Integer()),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
sa.column("header", sa.Unicode(length=255)),
|
||||
sa.column("prefix", sa.Unicode(length=255)),
|
||||
sa.column("is_fixture", sa.Boolean()),
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=0,
|
||||
name="KOT",
|
||||
header="",
|
||||
prefix="K",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=1,
|
||||
name="Combined",
|
||||
header="",
|
||||
prefix="INV",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=2,
|
||||
name="No Charge",
|
||||
header="NO CHARGE - THIS IS NOT A BILL - DON'T PAY",
|
||||
prefix="NC",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=4,
|
||||
name="Staff",
|
||||
header=" STAFF CONSUMPTION -- THIS IS NOT A BILL ",
|
||||
prefix="ST",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=8,
|
||||
name="Void",
|
||||
header="",
|
||||
prefix="V",
|
||||
is_fixture=True,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=16,
|
||||
name="Goods and Service Tax",
|
||||
header=" GSTIN: XXXXXXXXXXXXXXX \n Retail Invoice ",
|
||||
prefix="GST",
|
||||
is_fixture=False,
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
re.insert().values(
|
||||
id=32,
|
||||
name="Value Added Tax",
|
||||
header=" TIN: XXXXXXXXXXX \n Retail Invoice ",
|
||||
prefix="VAT",
|
||||
is_fixture=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_bills_table():
|
||||
voucher_type = ENUM("KOT", "REGULAR_BILL", "NO_CHARGE", "STAFF", "VOID", name="voucher_type", create_type=False)
|
||||
op.create_table(
|
||||
"bills",
|
||||
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("regime_id", sa.Integer(), nullable=False),
|
||||
sa.Column("voucher_id", sa.UUID(), nullable=False),
|
||||
sa.Column("is_valid", sa.Boolean(), nullable=False),
|
||||
sa.Column("bill_number", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["regime_id"], ["regimes.id"], name=op.f("fk_bills_regime_id_regimes")),
|
||||
sa.ForeignKeyConstraint(["voucher_id"], ["vouchers.id"], name=op.f("fk_bills_voucher_id_vouchers")),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_bills")),
|
||||
sa.UniqueConstraint("regime_id", "bill_number", name=op.f("uq_bills_regime_id")),
|
||||
sa.UniqueConstraint("voucher_id", "regime_id", name=op.f("uq_bills_voucher_id")),
|
||||
)
|
||||
|
||||
b = sa.table(
|
||||
"bills",
|
||||
sa.column("regime_id", sa.Integer()),
|
||||
sa.column("voucher_id", sa.UUID()),
|
||||
sa.column("is_valid", sa.Boolean()),
|
||||
sa.column("bill_number", sa.Integer()),
|
||||
)
|
||||
|
||||
v = sa.table(
|
||||
"vouchers",
|
||||
sa.column("id", sa.UUID()),
|
||||
sa.column("voucher_type", voucher_type),
|
||||
sa.column("bill_id", sa.Integer()),
|
||||
sa.column("kot_id", sa.Integer()),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
b.insert().from_select(
|
||||
[b.c.regime_id, b.c.voucher_id, b.c.is_valid, b.c.bill_number],
|
||||
sa.select(
|
||||
sa.case(
|
||||
(v.c.voucher_type == "KOT", sa.text("0")),
|
||||
(v.c.voucher_type == "REGULAR_BILL", sa.text("1")),
|
||||
(v.c.voucher_type == "NO_CHARGE", sa.text("2")),
|
||||
(v.c.voucher_type == "STAFF", sa.text("4")),
|
||||
(v.c.voucher_type == "VOID", sa.text("8")),
|
||||
),
|
||||
v.c.id,
|
||||
sa.text("True"),
|
||||
sa.case((v.c.bill_id == None, v.c.kot_id), else_=v.c.bill_id), # noqa: E711
|
||||
).where(
|
||||
v.c.bill_id != None # noqa: E711
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_taxes():
|
||||
op.add_column("taxes", sa.Column("regime_id", sa.Integer(), nullable=True))
|
||||
ta = sa.table(
|
||||
"taxes",
|
||||
sa.column("regime_id", sa.Integer()),
|
||||
)
|
||||
op.execute(ta.update().values(regime_id=1))
|
||||
op.alter_column("taxes", "regime_id", existing_type=sa.Integer(), nullable=False)
|
||||
op.create_foreign_key(op.f("fk_taxes_regime_id_regimes"), "taxes", "regimes", ["regime_id"], ["id"])
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("vouchers", sa.Column("bill_id", sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column("vouchers", sa.Column("date", postgresql.TIMESTAMP(), autoincrement=False, nullable=False))
|
||||
op.drop_index(op.f("ix_vouchers_creation_date"), table_name="vouchers")
|
||||
op.create_unique_constraint("uq_vouchers_bill_id", "vouchers", ["bill_id", "voucher_type"])
|
||||
op.create_index("ix_vouchers_date", "vouchers", ["date"], unique=False)
|
||||
op.drop_constraint(op.f("fk_taxes_regime_id_regimes"), "taxes", type_="foreignkey")
|
||||
op.drop_column("taxes", "regime_id")
|
||||
op.drop_index(op.f("ix_bills_date"), table_name="bills")
|
||||
op.drop_table("bills")
|
||||
op.drop_table("regimes")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,40 @@
|
||||
"""role includes
|
||||
|
||||
Revision ID: 5cb65066be86
|
||||
Revises: 367ecf7b898f
|
||||
Create Date: 2026-02-11 08:21:01.679893
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5cb65066be86"
|
||||
down_revision = "367ecf7b898f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"role_includes",
|
||||
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
|
||||
sa.Column("role_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("included_role_id", sa.Uuid(), nullable=False),
|
||||
sa.CheckConstraint("role_id <> included_role_id", name=op.f("ck_role_includes_no_self_include")),
|
||||
sa.ForeignKeyConstraint(
|
||||
["included_role_id"], ["roles.id"], name=op.f("fk_role_includes_included_role_id_roles")
|
||||
),
|
||||
sa.ForeignKeyConstraint(["role_id"], ["roles.id"], name=op.f("fk_role_includes_role_id_roles")),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_role_includes")),
|
||||
sa.UniqueConstraint("role_id", "included_role_id", name=op.f("uq_role_includes_role_id")),
|
||||
sa.Index(op.f("ix_role_includes_role_id"), "role_id"),
|
||||
sa.Index(op.f("ix_role_includes_included_role_id"), "included_role_id"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("role_includes")
|
||||
@@ -0,0 +1,182 @@
|
||||
"""booking
|
||||
|
||||
Revision ID: 7e9944b430d6
|
||||
Revises: 1d95eb10ea20
|
||||
Create Date: 2023-08-19 14:25:40.206187
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7e9944b430d6"
|
||||
down_revision = "1d95eb10ea20"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("customers", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"customers", "phone", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"customers", "address", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=True
|
||||
)
|
||||
op.alter_column("devices", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"food_tables", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"menu_categories", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"modifier_categories", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column("modifiers", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
overviewstatus = sa.Enum("running", "printed", name="overviewstatus")
|
||||
overviewstatus.create(op.get_bind())
|
||||
op.execute("ALTER TABLE overview ALTER COLUMN status TYPE overviewstatus USING status::text::overviewstatus;")
|
||||
op.alter_column(
|
||||
"overview",
|
||||
"status",
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
type_=overviewstatus,
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"permissions", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column("printers", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"printers", "address", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"printers", "cut_code", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"product_versions", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"product_versions", "units", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column("regimes", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("regimes", "header", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("regimes", "prefix", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("roles", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"sale_categories", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column("sections", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("settings", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("settings", "data", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False)
|
||||
op.alter_column(
|
||||
"settle_options", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False
|
||||
)
|
||||
op.alter_column("taxes", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("users", "name", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column("users", "password", existing_type=sa.VARCHAR(length=60), type_=sa.Text(), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"vouchers", "narration", existing_type=sa.VARCHAR(length=1000), type_=sa.Text(), existing_nullable=True
|
||||
)
|
||||
op.alter_column("vouchers", "reason", existing_type=sa.VARCHAR(length=255), type_=sa.Text(), existing_nullable=True)
|
||||
|
||||
guestbooktype = sa.Enum("walk_in", "booking", "arrived", name="guestbooktype")
|
||||
guestbooktype.create(op.get_bind())
|
||||
gbt = sa.table(
|
||||
"guest_book",
|
||||
sa.column("last_edit_date", sa.DateTime()),
|
||||
sa.column("arrival_date", sa.DateTime()),
|
||||
sa.column("type", guestbooktype),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("guest_book") as batch_op:
|
||||
batch_op.add_column(sa.Column("booking_date", sa.DateTime(), nullable=True))
|
||||
batch_op.alter_column("creation_date", new_column_name="last_edit_date")
|
||||
batch_op.add_column(sa.Column("arrival_date", sa.DateTime(), nullable=True))
|
||||
batch_op.add_column(sa.Column("notes", sa.Text(), nullable=True))
|
||||
batch_op.add_column(sa.Column("type", guestbooktype, nullable=True))
|
||||
op.execute(gbt.update().values(arrival_date=sa.text("last_edit_date")))
|
||||
op.execute(gbt.update().values(type="walk_in"))
|
||||
with op.batch_alter_table("guest_book") as batch_op:
|
||||
batch_op.alter_column("type", existing_type=guestbooktype, nullable=False)
|
||||
batch_op.create_check_constraint("nulls", sa.text("num_nonnulls(booking_date, arrival_date) > 0"))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("vouchers", "reason", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=True)
|
||||
op.alter_column(
|
||||
"vouchers", "narration", existing_type=sa.Text(), type_=sa.VARCHAR(length=1000), existing_nullable=True
|
||||
)
|
||||
op.alter_column("users", "password", existing_type=sa.Text(), type_=sa.VARCHAR(length=60), existing_nullable=False)
|
||||
op.alter_column("users", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column("taxes", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"settle_options", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column("settings", "data", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True)
|
||||
op.alter_column("settings", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column("sections", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"sale_categories", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column("roles", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column("regimes", "prefix", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column("regimes", "header", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column("regimes", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"product_versions", "units", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"product_versions", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"printers", "cut_code", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"printers", "address", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column("printers", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"permissions", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"overview",
|
||||
"status",
|
||||
existing_type=sa.Enum("running", "printed", name="overviewstatus"),
|
||||
type_=sa.VARCHAR(length=255),
|
||||
existing_nullable=False,
|
||||
)
|
||||
op.alter_column("modifiers", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"modifier_categories", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column(
|
||||
"menu_categories", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.add_column("guest_book", sa.Column("creation_date", postgresql.TIMESTAMP(), autoincrement=False, nullable=False))
|
||||
op.drop_column("guest_book", "type_")
|
||||
op.drop_column("guest_book", "notes")
|
||||
op.drop_column("guest_book", "last_edit_date")
|
||||
op.drop_column("guest_book", "date")
|
||||
op.alter_column(
|
||||
"food_tables", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column("devices", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
op.alter_column(
|
||||
"customers", "address", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=True
|
||||
)
|
||||
op.alter_column(
|
||||
"customers", "phone", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False
|
||||
)
|
||||
op.alter_column("customers", "name", existing_type=sa.Text(), type_=sa.VARCHAR(length=255), existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
@@ -5,12 +5,14 @@ Revises: c123dbf9c659
|
||||
Create Date: 2021-08-17 12:10:01.082632
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import column, table
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "81d94c5223a7"
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""index
|
||||
|
||||
Revision ID: 8260414066d6
|
||||
Revises: 5cb65066be86
|
||||
Create Date: 2026-02-13 06:22:39.926120
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "8260414066d6"
|
||||
down_revision = "5cb65066be86"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# 1) Add the column (nullable for backfill)
|
||||
op.add_column("sku_versions", sa.Column("product_id", sa.UUID(), nullable=True))
|
||||
|
||||
# 2) Define lightweight table objects (no autoload; explicit columns only)
|
||||
sku_versions = sa.Table(
|
||||
"sku_versions",
|
||||
sa.MetaData(),
|
||||
sa.Column("id", sa.UUID(), primary_key=True),
|
||||
sa.Column("sku_id", sa.UUID(), nullable=False),
|
||||
sa.Column("product_id", sa.UUID(), nullable=True),
|
||||
sa.Column("valid_from", sa.Date(), nullable=True),
|
||||
sa.Column("valid_till", sa.Date(), nullable=True),
|
||||
sa.Column("units", sa.Unicode(), nullable=False),
|
||||
)
|
||||
|
||||
stock_keeping_units = sa.Table(
|
||||
"stock_keeping_units",
|
||||
sa.MetaData(),
|
||||
sa.Column("id", sa.UUID(), primary_key=True),
|
||||
sa.Column("product_id", sa.UUID(), nullable=False),
|
||||
)
|
||||
|
||||
# 3) Backfill via SQLAlchemy Core UPDATE..SET..(SELECT ...)
|
||||
product_id_subq = (
|
||||
sa.select(stock_keeping_units.c.product_id)
|
||||
.where(stock_keeping_units.c.id == sku_versions.c.sku_id)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
backfill_stmt = (
|
||||
sa.update(sku_versions).values(product_id=product_id_subq).where(sku_versions.c.product_id.is_(None))
|
||||
)
|
||||
op.execute(backfill_stmt)
|
||||
|
||||
# 5) Enforce NOT NULL at schema level
|
||||
op.alter_column("sku_versions", "product_id", nullable=False)
|
||||
|
||||
# 6) Trigger keeps product_id in sync when sku_id changes
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION sku_versions_set_product_id()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
SELECT s.product_id INTO NEW.product_id
|
||||
FROM stock_keeping_units s
|
||||
WHERE s.id = NEW.sku_id;
|
||||
|
||||
IF NEW.product_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Invalid sku_id %, no product found', NEW.sku_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.execute(sa.text("DROP TRIGGER IF EXISTS trg_sku_versions_set_product_id ON sku_versions;"))
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TRIGGER trg_sku_versions_set_product_id
|
||||
BEFORE INSERT OR UPDATE OF sku_id
|
||||
ON sku_versions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION sku_versions_set_product_id();
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# 7) Exclusion constraint: product_id + units must not overlap in time
|
||||
# daterange(valid_from, valid_till, '[]') overlap operator &&.
|
||||
sv = sa.table(
|
||||
"sku_versions",
|
||||
sa.column("product_id", sa.UUID()),
|
||||
sa.column("units", sa.UUID()),
|
||||
sa.column("valid_from", sa.Date()),
|
||||
sa.column("valid_till", sa.Date()),
|
||||
)
|
||||
|
||||
op.create_exclude_constraint(
|
||||
op.f("uq_sku_versions_product_id_units"),
|
||||
"sku_versions",
|
||||
(sv.c.product_id, "="),
|
||||
(sv.c.units, "="),
|
||||
(sa.func.daterange(sv.c.valid_from, sv.c.valid_till, sa.text("'[]'")), "&&"),
|
||||
)
|
||||
|
||||
prod = sa.table(
|
||||
"product_versions",
|
||||
sa.column("id", sa.UUID()),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
sa.column("fraction_units", sa.Unicode(length=255)),
|
||||
sa.column("valid_from", sa.Date()),
|
||||
sa.column("valid_till", sa.Date()),
|
||||
)
|
||||
# Update the exclude constraint on product_versions to only be on the name and drop fraction_units from it
|
||||
op.drop_constraint("uq_product_versions_name", "product_versions", type_="unique")
|
||||
op.create_exclude_constraint(
|
||||
op.f("uq_product_versions_name"),
|
||||
"product_versions",
|
||||
(prod.c.name, "="),
|
||||
(sa.func.daterange(prod.c.valid_from, prod.c.valid_till, sa.text("'[]'")), "&&"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint("uq_sku_versions_product_id_units", "sku_versions", type_="exclude")
|
||||
|
||||
# Drop trigger + function
|
||||
op.execute(sa.text("DROP TRIGGER IF EXISTS trg_sku_versions_set_product_id ON sku_versions;"))
|
||||
op.execute(sa.text("DROP FUNCTION IF EXISTS sku_versions_set_product_id();"))
|
||||
|
||||
# Drop column
|
||||
op.drop_column("sku_versions", "product_id")
|
||||
|
||||
prod = sa.table(
|
||||
"product_versions",
|
||||
sa.column("id", sa.UUID()),
|
||||
sa.column("name", sa.Unicode(length=255)),
|
||||
sa.column("fraction_units", sa.Unicode(length=255)),
|
||||
sa.column("valid_from", sa.Date()),
|
||||
sa.column("valid_till", sa.Date()),
|
||||
)
|
||||
op.drop_constraint("uq_product_versions_name", "product_versions", type_="unique")
|
||||
op.create_exclude_constraint(
|
||||
"uq_product_versions_name",
|
||||
"product_versions",
|
||||
(prod.c.name, "="),
|
||||
(prod.c.units, "="),
|
||||
(sa.func.daterange(prod.c.valid_from, prod.c.valid_till, sa.text("'[]'")), "&&"),
|
||||
)
|
||||
@@ -5,16 +5,18 @@ Revises:
|
||||
Create Date: 2020-06-04 08:14:34.132248
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy import column, table
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
from barker.models.db_setting import DbSetting
|
||||
from barker.models.modifier_category import ModifierCategory
|
||||
from barker.models.permission import Permission
|
||||
from barker.models.section import Section
|
||||
from barker.models.user import User # noqa: F401
|
||||
from sqlalchemy import column, table
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""normalize sku
|
||||
|
||||
Revision ID: c116517977af
|
||||
Revises: 32c508eed4df
|
||||
Create Date: 2026-01-23 15:33:39.682395
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c116517977af"
|
||||
down_revision = "32c508eed4df"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) Build mapping: old_product_id -> canonical_product_id
|
||||
# Canonical = product whose CURRENT pv (valid_till IS NULL) has
|
||||
# latest valid_from (prefer newer). Tie-break: product_id.
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE TEMP TABLE tmp_product_canon (
|
||||
old_product_id uuid PRIMARY KEY,
|
||||
canonical_product_id uuid NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH current_pv AS (
|
||||
SELECT
|
||||
pv.product_id,
|
||||
pv.name,
|
||||
pv.valid_from
|
||||
FROM product_versions pv
|
||||
WHERE pv.valid_till IS NULL
|
||||
),
|
||||
canon AS (
|
||||
SELECT DISTINCT ON (name)
|
||||
name,
|
||||
product_id AS canonical_product_id
|
||||
FROM current_pv
|
||||
ORDER BY name, valid_from DESC NULLS LAST, product_id
|
||||
)
|
||||
INSERT INTO tmp_product_canon (old_product_id, canonical_product_id)
|
||||
SELECT
|
||||
cp.product_id AS old_product_id,
|
||||
c.canonical_product_id
|
||||
FROM current_pv cp
|
||||
JOIN canon c ON c.name = cp.name;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 2) Move SKUs to canonical product
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE stock_keeping_units sku
|
||||
SET product_id = m.canonical_product_id
|
||||
FROM tmp_product_canon m
|
||||
WHERE sku.product_id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 3) Merge modifier_categories_products:
|
||||
# (A) delete duplicates that would conflict after update
|
||||
# (B) update remaining rows to canonical_product_id
|
||||
# ---------------------------------------------------------------------
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM modifier_categories_products mcp
|
||||
USING tmp_product_canon m
|
||||
WHERE m.old_product_id <> m.canonical_product_id
|
||||
AND mcp.product_id = m.old_product_id
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM modifier_categories_products mcp2
|
||||
WHERE mcp2.product_id = m.canonical_product_id
|
||||
AND mcp2.modifier_category_id = mcp.modifier_category_id
|
||||
);
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE modifier_categories_products mcp
|
||||
SET product_id = m.canonical_product_id
|
||||
FROM tmp_product_canon m
|
||||
WHERE m.old_product_id <> m.canonical_product_id
|
||||
AND mcp.product_id = m.old_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 4) Delete duplicate product_versions and products.
|
||||
# Since you said "newer products mean more than old ones",
|
||||
# we keep canonical product and delete other products in the group.
|
||||
#
|
||||
# IMPORTANT:
|
||||
# - inventories already point to SKUs, so safe
|
||||
# - SKUs already moved to canonical product
|
||||
# - modifier categories already moved
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Delete product_versions for old (non-canonical) products
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM product_versions pv
|
||||
USING tmp_product_canon m
|
||||
WHERE pv.product_id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# Delete products for old (non-canonical) products
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE FROM products p
|
||||
USING tmp_product_canon m
|
||||
WHERE p.id = m.old_product_id
|
||||
AND m.old_product_id <> m.canonical_product_id;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -5,6 +5,7 @@ Revises: e5e8acfc6495
|
||||
Create Date: 2021-08-06 10:15:57.719313
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
@@ -5,13 +5,15 @@ Revises: 48bc1c7c07ce
|
||||
Create Date: 2021-06-27 09:57:43.034956
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "e5e8acfc6495"
|
||||
down_revision = "48bc1c7c07ce"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""fix nullability
|
||||
|
||||
Revision ID: f135019a4ebf
|
||||
Revises: 0e326930b8a4
|
||||
Create Date: 2023-02-22 03:41:23.537388
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f135019a4ebf"
|
||||
down_revision = "0e326930b8a4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("inventories", "quantity", existing_type=sa.NUMERIC(precision=15, scale=2), nullable=False)
|
||||
op.alter_column("inventories", "price", existing_type=sa.NUMERIC(precision=15, scale=2), nullable=False)
|
||||
op.alter_column("inventories", "tax_rate", existing_type=sa.NUMERIC(precision=15, scale=5), nullable=False)
|
||||
op.alter_column("inventories", "discount", existing_type=sa.NUMERIC(precision=15, scale=5), nullable=False)
|
||||
op.alter_column("permissions", "name", existing_type=sa.VARCHAR(length=255), nullable=False)
|
||||
op.alter_column("roles", "name", existing_type=sa.VARCHAR(length=255), nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column("roles", "name", existing_type=sa.VARCHAR(length=255), nullable=True)
|
||||
op.alter_column("permissions", "name", existing_type=sa.VARCHAR(length=255), nullable=True)
|
||||
op.alter_column("inventories", "discount", existing_type=sa.NUMERIC(precision=15, scale=5), nullable=True)
|
||||
op.alter_column("inventories", "tax_rate", existing_type=sa.NUMERIC(precision=15, scale=5), nullable=True)
|
||||
op.alter_column("inventories", "price", existing_type=sa.NUMERIC(precision=15, scale=2), nullable=True)
|
||||
op.alter_column("inventories", "quantity", existing_type=sa.NUMERIC(precision=15, scale=2), nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -1 +1 @@
|
||||
__version__ = "10.3.1"
|
||||
__version__ = "15.1.6"
|
||||
@@ -1,5 +0,0 @@
|
||||
from arq.connections import RedisSettings
|
||||
from barker.core.config import settings as sett
|
||||
|
||||
|
||||
settings = RedisSettings(host=sett.REDIS_HOST, port=sett.REDIS_PORT)
|
||||
@@ -1,7 +1,7 @@
|
||||
import secrets
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -18,19 +18,13 @@ class Settings(BaseSettings):
|
||||
LOG_LEVEL: str = "NOTSET"
|
||||
SQLALCHEMY_DATABASE_URI: str = ""
|
||||
|
||||
REDIS_HOST: str = "127.0.0.1"
|
||||
REDIS_PORT: int = 6379
|
||||
|
||||
ALEMBIC_LOG_LEVEL: str = "INFO"
|
||||
ALEMBIC_SQLALCHEMY_LOG_LEVEL: str = "WARN"
|
||||
|
||||
WORKER_REDIS_HOST: str = "127.0.0.1"
|
||||
WORKER_REDIS_PORT: int = 6379
|
||||
WORKER_QUEUE_NAME: str = "arq:queue"
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
env_file = ".env"
|
||||
NATS_URL: str = "nats://127.0.0.1:4222"
|
||||
SUBJECT: str = "barker.print"
|
||||
STREAM: str = "barker_print_stream"
|
||||
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env")
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Annotated, Any
|
||||
|
||||
import jwt
|
||||
|
||||
from fastapi import Depends, HTTPException, Security, status
|
||||
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
||||
from jose import jwt
|
||||
from jose.exceptions import ExpiredSignatureError
|
||||
from jwt import PyJWTError
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlalchemy import select
|
||||
@@ -32,18 +33,9 @@ class TokenData(BaseModel):
|
||||
scopes: list[str] = []
|
||||
|
||||
|
||||
def f7(seq):
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||
|
||||
|
||||
def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
||||
def create_access_token(*, data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
expire = datetime.now(UTC).replace(tzinfo=None) + (expires_delta or timedelta(minutes=15))
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
@@ -65,22 +57,16 @@ def authenticate_user(username: str, password: str, db: Session) -> UserModel |
|
||||
|
||||
|
||||
def device_allowed(user: UserModel, device_id: uuid.UUID | None, db: Session) -> tuple[bool, Device]:
|
||||
device: Device = db.execute(select(Device).where(Device.id == device_id)).scalars().one_or_none()
|
||||
device = db.execute(select(Device).where(Device.id == device_id)).scalars().one_or_none()
|
||||
if device is None:
|
||||
device = Device.create(db)
|
||||
allowed = "add-devices" in set([p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions])
|
||||
allowed = "add-devices" in {p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions}
|
||||
|
||||
return allowed or device.enabled, device
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
security_scopes: SecurityScopes,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> UserToken:
|
||||
if security_scopes.scopes:
|
||||
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
|
||||
else:
|
||||
authenticate_value = "Bearer"
|
||||
async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)]) -> UserToken:
|
||||
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' if security_scopes.scopes else "Bearer"
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
@@ -88,16 +74,16 @@ async def get_current_user(
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
username = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_scopes = payload.get("scopes", [])
|
||||
token_data = TokenData(scopes=token_scopes, username=username)
|
||||
except (PyJWTError, ValidationError, ExpiredSignatureError):
|
||||
except PyJWTError, ValidationError:
|
||||
raise credentials_exception
|
||||
user = get_user(
|
||||
username=token_data.username,
|
||||
id_=payload.get("userId", None),
|
||||
id_=payload.get("userId", ""),
|
||||
locked_out=payload.get("lockedOut", True),
|
||||
scopes=token_scopes,
|
||||
)
|
||||
@@ -114,7 +100,7 @@ async def get_current_user(
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: UserToken = Security(get_current_user, scopes=["authenticated"])
|
||||
current_user: Annotated[UserToken, Security(get_current_user, scopes=["authenticated"])],
|
||||
) -> UserToken:
|
||||
if current_user.locked_out:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
from datetime import date, timedelta
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_date(session) -> str:
|
||||
def get_date(session: dict[str, Any]) -> str:
|
||||
if "date" not in session:
|
||||
session["date"] = date.today().strftime("%d-%b-%Y")
|
||||
return session["date"]
|
||||
session["date"] = date.today().isoformat()
|
||||
return session["date"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def set_date(date_, session):
|
||||
session["date"] = date_
|
||||
return session["date"]
|
||||
def set_date(date_: str | date, session: dict[str, Any]) -> None:
|
||||
session["date"] = date_ if isinstance(date_, str) else date_.isoformat()
|
||||
|
||||
|
||||
def get_start_date(session):
|
||||
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"]
|
||||
session["start"] = get_first_day(date.today()).isoformat()
|
||||
return session["start"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def get_finish_date(session):
|
||||
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"]
|
||||
session["finish"] = get_last_day(date.today()).isoformat()
|
||||
return session["finish"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def set_period(start, finish, session):
|
||||
session["start"] = start if isinstance(start, str) else start.strftime("%d-%b-%Y")
|
||||
session["finish"] = finish if isinstance(finish, str) else finish.strftime("%d-%b-%Y")
|
||||
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.isoformat()
|
||||
if finish is not None:
|
||||
session["finish"] = finish if isinstance(finish, str) else finish.isoformat()
|
||||
|
||||
|
||||
def get_first_day(dt, d_years=0, d_months=0):
|
||||
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):
|
||||
def get_last_day(dt: date) -> date:
|
||||
return get_first_day(dt, 0, 1) + timedelta(-1)
|
||||
@@ -0,0 +1,56 @@
|
||||
import asyncio
|
||||
|
||||
from nats.js.api import StreamConfig
|
||||
from taskiq import BrokerMessage
|
||||
from taskiq_nats import PullBasedJetStreamBroker
|
||||
|
||||
from .config import settings
|
||||
|
||||
|
||||
class RoutedJetStreamBroker(PullBasedJetStreamBroker):
|
||||
async def kick(self, message: BrokerMessage) -> None:
|
||||
subject = message.labels.get("queue", self.subject)
|
||||
await self.js.publish(
|
||||
subject,
|
||||
payload=message.message,
|
||||
headers=message.labels,
|
||||
)
|
||||
|
||||
|
||||
broker = RoutedJetStreamBroker(
|
||||
settings.NATS_URL,
|
||||
subject=f"{settings.SUBJECT}.>",
|
||||
stream_name=f"{settings.STREAM}",
|
||||
stream_config=StreamConfig(
|
||||
name=f"{settings.STREAM}",
|
||||
subjects=[f"{settings.SUBJECT}.>"],
|
||||
),
|
||||
)
|
||||
|
||||
main_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
@broker.task(task_name="nat_print")
|
||||
async def nat_print(data: str, cut_code: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def enqueue_print_job_sync(printer_name: str, data: str, cut_code: str) -> None:
|
||||
coro = nat_print.kicker().with_labels(queue=f"{settings.SUBJECT}.{printer_name}").kiq(data, cut_code)
|
||||
|
||||
if main_loop is not None and main_loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(coro, main_loop)
|
||||
else:
|
||||
|
||||
async def _enqueue() -> None:
|
||||
await broker.startup()
|
||||
try:
|
||||
await coro
|
||||
finally:
|
||||
await broker.shutdown()
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(_enqueue())
|
||||
finally:
|
||||
loop.close()
|
||||
+86
-36
@@ -1,38 +1,88 @@
|
||||
# Import all the models, so that Base has them before being
|
||||
# imported by Alembic
|
||||
from barker.db.base_class import Base # noqa: F401
|
||||
from barker.models.customer import Customer # noqa: F401
|
||||
from barker.models.customer_discount import CustomerDiscount # noqa: F401
|
||||
from barker.models.db_setting import DbSetting # noqa: F401
|
||||
from barker.models.device import Device # noqa: F401
|
||||
from barker.models.food_table import FoodTable # noqa: F401
|
||||
from barker.models.guest_book import GuestBook # noqa: F401
|
||||
from barker.models.inventory import Inventory # noqa: F401
|
||||
from barker.models.inventory_modifier import InventoryModifier # noqa: F401
|
||||
from barker.models.kot import Kot # noqa: F401
|
||||
from barker.models.login_history import LoginHistory # noqa: F401
|
||||
from barker.models.menu_category import MenuCategory # noqa: F401
|
||||
from barker.models.modifier import Modifier # noqa: F401
|
||||
from barker.models.modifier_categories_products import ( # noqa: F401
|
||||
modifier_categories_products,
|
||||
)
|
||||
from barker.models.modifier_category import ModifierCategory # noqa: F401
|
||||
from barker.models.overview import Overview # noqa: F401
|
||||
from barker.models.permission import Permission # noqa: F401
|
||||
from barker.models.printer import Printer # noqa: F401
|
||||
from barker.models.product import Product # noqa: F401
|
||||
from barker.models.product_version import ProductVersion # noqa: F401
|
||||
from barker.models.reporting_level import ReportingLevel # noqa: F401
|
||||
from barker.models.reprint import Reprint # noqa: F401
|
||||
from barker.models.role import Role # noqa: F401
|
||||
from barker.models.role_permissions import role_permissions # noqa: F401
|
||||
from barker.models.sale_category import SaleCategory # noqa: F401
|
||||
from barker.models.section import Section # noqa: F401
|
||||
from barker.models.section_printer import SectionPrinter # noqa: F401
|
||||
from barker.models.settle_option import SettleOption # noqa: F401
|
||||
from barker.models.settlement import Settlement # noqa: F401
|
||||
from barker.models.tax import Tax # noqa: F401
|
||||
from barker.models.user import User # noqa: F401
|
||||
from barker.models.user_roles import user_roles # noqa: F401
|
||||
from barker.models.voucher import Voucher # noqa
|
||||
from barker.models.voucher_type import VoucherType # noqa: F401
|
||||
from ..models.bill import Bill
|
||||
from ..models.bundle_item import BundleItem
|
||||
from ..models.customer import Customer
|
||||
from ..models.customer_discount import CustomerDiscount
|
||||
from ..models.db_setting import DbSetting
|
||||
from ..models.device import Device
|
||||
from ..models.food_table import FoodTable
|
||||
from ..models.guest_book import GuestBook
|
||||
from ..models.inventory import Inventory
|
||||
from ..models.inventory_modifier import InventoryModifier
|
||||
from ..models.inventory_type import InventoryType
|
||||
from ..models.kot import Kot
|
||||
from ..models.login_history import LoginHistory
|
||||
from ..models.menu_category import MenuCategory
|
||||
from ..models.modifier import Modifier
|
||||
from ..models.modifier_category import ModifierCategory
|
||||
from ..models.modifier_category_product import ModifierCategoryProduct
|
||||
from ..models.overview import Overview
|
||||
from ..models.overview_status import OverviewStatus
|
||||
from ..models.permission import Permission
|
||||
from ..models.printer import Printer
|
||||
from ..models.product import Product
|
||||
from ..models.product_version import ProductVersion
|
||||
from ..models.regime import Regime
|
||||
from ..models.reporting_level import ReportingLevel
|
||||
from ..models.reprint import Reprint
|
||||
from ..models.role import Role
|
||||
from ..models.role_permission import RolePermission
|
||||
from ..models.sale_category import SaleCategory
|
||||
from ..models.section import Section
|
||||
from ..models.section_printer import SectionPrinter
|
||||
from ..models.settle_option import SettleOption
|
||||
from ..models.settlement import Settlement
|
||||
from ..models.sku_version import SkuVersion
|
||||
from ..models.stock_keeping_unit import StockKeepingUnit
|
||||
from ..models.tax import Tax
|
||||
from ..models.user import User
|
||||
from ..models.user_role import UserRole
|
||||
from ..models.voucher import Voucher
|
||||
from ..models.voucher_type import VoucherType
|
||||
from .base_class import reg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Bill",
|
||||
"BundleItem",
|
||||
"Customer",
|
||||
"CustomerDiscount",
|
||||
"DbSetting",
|
||||
"Device",
|
||||
"FoodTable",
|
||||
"GuestBook",
|
||||
"Inventory",
|
||||
"InventoryModifier",
|
||||
"InventoryType",
|
||||
"Kot",
|
||||
"LoginHistory",
|
||||
"MenuCategory",
|
||||
"Modifier",
|
||||
"ModifierCategory",
|
||||
"ModifierCategoryProduct",
|
||||
"Overview",
|
||||
"OverviewStatus",
|
||||
"Permission",
|
||||
"Printer",
|
||||
"Product",
|
||||
"ProductVersion",
|
||||
"Regime",
|
||||
"ReportingLevel",
|
||||
"Reprint",
|
||||
"Role",
|
||||
"RolePermission",
|
||||
"SaleCategory",
|
||||
"Section",
|
||||
"SectionPrinter",
|
||||
"SettleOption",
|
||||
"Settlement",
|
||||
"SkuVersion",
|
||||
"StockKeepingUnit",
|
||||
"Tax",
|
||||
"User",
|
||||
"UserRole",
|
||||
"Voucher",
|
||||
"VoucherType",
|
||||
"reg",
|
||||
]
|
||||
@@ -14,5 +14,5 @@ NAMING_CONVENTION = {
|
||||
}
|
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION)
|
||||
mapper_registry = registry(metadata)
|
||||
Base = mapper_registry.generate_base()
|
||||
reg = registry()
|
||||
reg.metadata = metadata
|
||||
@@ -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,217 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from barker.core.config import settings # noqa: F401
|
||||
from barker.models.customer import Customer
|
||||
from barker.models.db_setting import DbSetting
|
||||
from barker.models.modifier_category import ModifierCategory
|
||||
from barker.models.permission import Permission
|
||||
from barker.models.role import Role
|
||||
from barker.models.section import Section
|
||||
from barker.models.settle_option import SettleOption
|
||||
from barker.models.user import User
|
||||
from brewman.db import base # noqa: F401
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB
|
||||
# otherwise, SQL Alchemy might fail to initialize relationships properly
|
||||
# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
|
||||
|
||||
|
||||
def init_db(db: Session) -> None:
|
||||
# Tables should be created with Alembic migrations
|
||||
# But if you don't want to use migrations, create
|
||||
# the tables un-commenting the next line
|
||||
# Base.metadata.create_all(bind=engine)
|
||||
|
||||
prod = True
|
||||
if prod:
|
||||
db.add(Permission("Guest Book", uuid.UUID("7669dfc9-cc75-4e48-b267-145c8832a83c")))
|
||||
db.add(Permission("Printers", uuid.UUID("5b66c6f6-003a-4ef8-ba28-49b8ff1ac33c")))
|
||||
db.add(Permission("Sections", uuid.UUID("c973f32c-a37b-496a-8dc5-60d2e4c39e97")))
|
||||
db.add(
|
||||
Permission(
|
||||
"Section Printers",
|
||||
uuid.UUID("7a04ba63-5d08-4078-9051-a6d91cce3e48"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
add_permissions(db)
|
||||
|
||||
main_section = Section("Main", uuid.UUID("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df"))
|
||||
db.add(main_section)
|
||||
mcs = [
|
||||
ModifierCategory(
|
||||
"Old Modifiers",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("e046ad33-dc65-4c78-8833-c3d3538d44c0"),
|
||||
),
|
||||
ModifierCategory(
|
||||
"Bar Instructions",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("b572f401-3c2f-48b9-8973-ada5a6e4d3a6"),
|
||||
),
|
||||
ModifierCategory(
|
||||
"Delivery",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("caa72832-5034-405e-8442-68a8cc12ace9"),
|
||||
),
|
||||
ModifierCategory(
|
||||
"Kitchen Instructions",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("d6a0595f-e209-42e4-bb12-b7499f9a9c4d"),
|
||||
),
|
||||
ModifierCategory(
|
||||
"Mixers",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("60ca9122-adc5-463b-ad5f-33a68df8c3ae"),
|
||||
),
|
||||
ModifierCategory(
|
||||
"Pasta Sauce",
|
||||
0,
|
||||
0,
|
||||
True,
|
||||
0,
|
||||
uuid.UUID("ef5b1a0b-5eb1-45ff-bd82-3209c8b888df"),
|
||||
),
|
||||
]
|
||||
for item in mcs:
|
||||
db.add(item)
|
||||
|
||||
options = [
|
||||
SettleOption("Unsettled", False, 1, True, 1),
|
||||
SettleOption("Cash", True, 2, False, 2),
|
||||
SettleOption("Credit Card", True, 2, True, 3),
|
||||
SettleOption("No Charge", True, 3, True, 4),
|
||||
SettleOption("Bill To Company", True, 2, True, 5),
|
||||
SettleOption("Tip", True, 2, True, 6),
|
||||
SettleOption("Round Off", False, 1, False, 7),
|
||||
SettleOption("Amount", False, 1, False, 8),
|
||||
SettleOption("Void", True, 1, True, 9),
|
||||
SettleOption("Staff", True, 4, True, 10),
|
||||
]
|
||||
for option in options:
|
||||
db.add(option)
|
||||
db.add(Customer("Cash", "", "", uuid.UUID("2c716f4b-0736-429a-ad51-610d7c47cb5e")))
|
||||
db.add(
|
||||
DbSetting(
|
||||
uuid.UUID("fb738ba2-a3c9-40ed-891c-b930e6454974"),
|
||||
"Header",
|
||||
{
|
||||
"Text": """ Hops n Grains
|
||||
The Microbrewery
|
||||
SCO 358, Sector 9, Panchkula
|
||||
A Unit of Peitho Foods Pvt. Ltd.
|
||||
CIN: U15139CH2010PTC032202
|
||||
(Reg Add: Plot No. 907, Indl Area II, Chd)
|
||||
TIN: 06592507323
|
||||
Service Tax: AAFCP5097GSD001
|
||||
"""
|
||||
},
|
||||
)
|
||||
)
|
||||
db.add(
|
||||
DbSetting(
|
||||
uuid.UUID("f7799871-d16e-4c4d-9b57-2299a5839acb"),
|
||||
"Footer",
|
||||
{"Text": "Call: 0172-4026666, 8054923853, 8054923856"},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def add_permissions(db: Session):
|
||||
permissions = [
|
||||
Permission("Audit", uuid.UUID("f30fd1fb-df09-46f5-8c5d-181fd46f38de")),
|
||||
Permission("Beer Consumption", uuid.UUID("efbb8f31-9631-4491-92f4-17cc98e6a0c0")),
|
||||
Permission("Bill Details", uuid.UUID("612bb529-b50d-4653-a1c0-ebb725c7d728")),
|
||||
Permission("Cashier Checkout", uuid.UUID("a86f8bcf-66f4-4c44-89e1-b714488b8331")),
|
||||
Permission("Change Rate", uuid.UUID("10c63aae-0e48-4e54-b3b8-dd8a80b88fbf")),
|
||||
Permission("Customers", uuid.UUID("e5fef133-cdbe-441f-bb54-1f0db0c5db79")),
|
||||
Permission("Discount", uuid.UUID("1f66f131-0105-4466-8f8e-21e0ccc2ac27")),
|
||||
Permission("Discount Report", uuid.UUID("0d8ba1d5-6731-417c-ab0e-be03cfdc96db")),
|
||||
Permission("Edit Printed Bill", uuid.UUID("4ff6cb0f-93cb-4760-8219-4de280eaa957")),
|
||||
Permission(
|
||||
"Edit Printed Product",
|
||||
uuid.UUID("959713be-d753-4e14-8ecd-e27f33587499"),
|
||||
),
|
||||
Permission("Guest Book", uuid.UUID("7669dfc9-cc75-4e48-b267-145c8832a83c")),
|
||||
Permission("Machines", uuid.UUID("f12b573f-edcb-490d-91c3-fa76f6502ffd")),
|
||||
Permission("Merge Kots", uuid.UUID("bed774e9-4857-43b0-a4af-40230c9eb5db")),
|
||||
Permission("Merge Tables", uuid.UUID("dfc493fb-ac14-4602-8596-f93f47b617de")),
|
||||
Permission("Modifiers", uuid.UUID("9dc82529-1e86-41b4-a152-eaea9f775fea")),
|
||||
Permission(
|
||||
"Move Kot to New Table",
|
||||
uuid.UUID("5ed5796a-ca99-437f-8f67-483b1ade81ed"),
|
||||
),
|
||||
Permission("Move Table", uuid.UUID("cfdb69c9-d37a-40af-bef2-e29c04602543")),
|
||||
Permission("NC Product", uuid.UUID("54263587-773e-4cbb-b1e4-4e814141158e")),
|
||||
Permission("Open Bill", uuid.UUID("5811d233-a1ae-4d32-af52-cebbf46d274a")),
|
||||
Permission("Owner", uuid.UUID("6d109b66-8452-42aa-bbe9-d4cef440b7a1")),
|
||||
Permission("Print Bill", uuid.UUID("92242eae-bd38-4223-829b-2f981933b7f2")),
|
||||
Permission("Print Kot", uuid.UUID("0eef56fb-7741-49fd-8b84-5069cccae767")),
|
||||
Permission("Products", uuid.UUID("f1f892ed-34aa-4c50-be12-dbaf48be7d4c")),
|
||||
Permission("Roles", uuid.UUID("d39f86f6-14e4-4fc3-b07e-01e2501a6f65")),
|
||||
Permission("Sales", uuid.UUID("92ba45c4-2d34-4cbd-b5b8-d9164c3595d5")),
|
||||
Permission("Sales Analysis", uuid.UUID("0c1acb7c-9116-46e2-a3be-9d632e811330")),
|
||||
Permission("Sales Detail", uuid.UUID("e6476396-ef83-4c47-bc20-9f3b936813f9")),
|
||||
Permission("Settle Bill", uuid.UUID("9f7b658a-25bc-4a53-8560-2dde1c7a188a")),
|
||||
Permission("Split Bill", uuid.UUID("0a8cfaf4-624e-478f-be65-c3b6392078c7")),
|
||||
Permission("Tables", uuid.UUID("ac4512e8-efdd-429e-a7ae-a34d18782663")),
|
||||
Permission("Tax Analysis", uuid.UUID("bab36396-53dd-44ea-ab3c-426efa7830c8")),
|
||||
Permission("Taxes", uuid.UUID("5c8fcdde-460d-4047-810f-e34fb899fadc")),
|
||||
Permission("Users", uuid.UUID("243447b8-b403-47e6-8b3d-8e76f4df44a9")),
|
||||
Permission("Void Bill", uuid.UUID("e3c76262-adc0-4936-8b4d-217c6292298b")),
|
||||
Permission(
|
||||
"Void or Reprinted Bill Report",
|
||||
uuid.UUID("30c8e743-c710-42d7-843a-0b75543b3516"),
|
||||
),
|
||||
Permission("Printers", uuid.UUID("5b66c6f6-003a-4ef8-ba28-49b8ff1ac33c")),
|
||||
Permission("Sections", uuid.UUID("c973f32c-a37b-496a-8dc5-60d2e4c39e97")),
|
||||
]
|
||||
|
||||
for permission in permissions:
|
||||
db.add(permission)
|
||||
|
||||
roles = [
|
||||
Role("Owner", uuid.UUID("06ec0ddb-949c-4357-aefb-65e5e55a9ae7")),
|
||||
Role("Accountant", uuid.UUID("57d4538c-257a-4bf4-9a42-342992cb6af3")),
|
||||
Role("Bar Captain", uuid.UUID("5a8ebb05-4817-45a5-866f-e523c30bfa25")),
|
||||
Role("Captain", uuid.UUID("8db7001f-0964-4607-84f5-b6f4d9183fef")),
|
||||
Role("Cashier", uuid.UUID("f1f0b112-1404-4b55-b121-f07ee6e08d12")),
|
||||
Role("Controller", uuid.UUID("d9a5a478-1fe4-4847-84da-63cfba1a094a")),
|
||||
Role("Manager", uuid.UUID("90ea7b14-9057-4bfd-a7cf-0ee7a3bb2463")),
|
||||
Role("Senior Captain", uuid.UUID("d9b1b433-1ed5-4109-8ab2-cbd48ff010cd")),
|
||||
Role("Server", uuid.UUID("6b378b71-f091-4485-a589-8db94ff1d6a4")),
|
||||
]
|
||||
|
||||
roles[0].permissions = permissions
|
||||
roles[1].permissions = list(permissions[i] for i in [2, 3, 11, 18, 24, 25, 26, 30, 34])
|
||||
roles[2].permissions = list(permissions[i] for i in [26])
|
||||
roles[3].permissions = list(permissions[i] for i in [16])
|
||||
roles[4].permissions = list(permissions[i] for i in [20, 24, 27])
|
||||
roles[5].permissions = list(permissions[i] for i in [0, 2, 7, 11, 18, 22, 24, 25, 26, 34])
|
||||
roles[6].permissions = list(permissions[i] for i in [2, 3, 4, 5, 6, 8, 11, 14, 18, 22, 25, 26, 28, 29, 33, 34])
|
||||
roles[7].permissions = list(permissions[i] for i in [9, 12, 13, 15, 16])
|
||||
roles[8].permissions = list(permissions[i] for i in [21, 24])
|
||||
|
||||
for role in roles:
|
||||
db.add(role)
|
||||
|
||||
admin = User("Admin", "123456", False)
|
||||
admin.roles.append(roles[0])
|
||||
db.add(admin)
|
||||
@@ -1,12 +1,28 @@
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
|
||||
SessionFuture = sessionmaker(autoflush=False, bind=engine, autocommit=False)
|
||||
|
||||
|
||||
def get_session() -> Generator[Session]:
|
||||
with SessionFuture() as session:
|
||||
try:
|
||||
yield session
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
+64
-9
@@ -1,24 +1,37 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from .core import taskiq
|
||||
from .core.config import settings
|
||||
from .db.base_class import Base
|
||||
from .db.session import engine
|
||||
from .db.base import reg # noqa: F401
|
||||
from .db.friendly_db_error import constraint_to_friendly
|
||||
from .routers import (
|
||||
bundle,
|
||||
client_log,
|
||||
customer,
|
||||
customer_discount,
|
||||
db_settings,
|
||||
device,
|
||||
guest_book,
|
||||
header_footer,
|
||||
health,
|
||||
login,
|
||||
menu_category,
|
||||
modifier,
|
||||
modifier_category,
|
||||
printer,
|
||||
product,
|
||||
regime,
|
||||
role,
|
||||
sale_category,
|
||||
section,
|
||||
@@ -35,12 +48,14 @@ from .routers.reports import (
|
||||
bill_settlement_report,
|
||||
cashier_report,
|
||||
discount_report,
|
||||
menu_engineering_report,
|
||||
product_sale_report,
|
||||
product_updates_report,
|
||||
sale_report,
|
||||
tax_report,
|
||||
)
|
||||
from .routers.voucher import (
|
||||
cancel,
|
||||
change,
|
||||
merge_move,
|
||||
receive_payment,
|
||||
@@ -48,13 +63,42 @@ from .routers.voucher import (
|
||||
show,
|
||||
split,
|
||||
update,
|
||||
void,
|
||||
)
|
||||
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||
taskiq.main_loop = asyncio.get_running_loop()
|
||||
await taskiq.broker.startup()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await taskiq.broker.shutdown()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
|
||||
@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)
|
||||
@@ -70,6 +114,7 @@ app.include_router(printer.router, prefix="/api/printers", tags=["printers"])
|
||||
|
||||
app.include_router(menu_category.router, prefix="/api/menu-categories", tags=["products"])
|
||||
app.include_router(product.router, prefix="/api/products", tags=["products"])
|
||||
app.include_router(bundle.router, prefix="/api/bundles", tags=["products"])
|
||||
app.include_router(temporal_product.router, prefix="/api/temporal-products", tags=["products"])
|
||||
app.include_router(device.router, prefix="/api/devices", tags=["devices"])
|
||||
app.include_router(sale_category.router, prefix="/api/sale-categories", tags=["products"])
|
||||
@@ -84,6 +129,7 @@ app.include_router(settle_option.router, prefix="/api/settle-options", tags=["se
|
||||
app.include_router(db_settings.router, prefix="/api/settings", tags=["settings"])
|
||||
|
||||
app.include_router(tax.router, prefix="/api/taxes", tags=["taxes"])
|
||||
app.include_router(regime.router, prefix="/api/regimes", tags=["regimes"])
|
||||
|
||||
app.include_router(table.router, prefix="/api/tables", tags=["sections"])
|
||||
|
||||
@@ -102,6 +148,7 @@ app.include_router(discount_report.router, prefix="/api/discount-report", tags=[
|
||||
app.include_router(product_sale_report.router, prefix="/api/product-sale-report", tags=["reports"])
|
||||
app.include_router(sale_report.router, prefix="/api/sale-report", tags=["reports"])
|
||||
app.include_router(tax_report.router, prefix="/api/tax-report", tags=["reports"])
|
||||
app.include_router(menu_engineering_report.router, prefix="/api/menu-engineering-report", tags=["reports"])
|
||||
|
||||
app.include_router(guest_book.router, prefix="/api/guest-book", tags=["guest-book"])
|
||||
app.include_router(customer.router, prefix="/api/customers", tags=["guest-book"])
|
||||
@@ -110,11 +157,19 @@ app.include_router(show.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(save.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(update.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(receive_payment.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(void.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(cancel.router, prefix="/api/voucher", tags=["voucher"])
|
||||
app.include_router(merge_move.router, prefix="/api", tags=["voucher"])
|
||||
app.include_router(split.router, prefix="/api", tags=["voucher"])
|
||||
app.include_router(change.router, prefix="/api/voucher", tags=["voucher"])
|
||||
|
||||
app.include_router(health.router, prefix="/health", tags=["health"])
|
||||
app.include_router(client_log.router, prefix="/api/client-logs", tags=["logs"])
|
||||
app.frontend("/", directory="frontend", check_dir="auto")
|
||||
|
||||
def init():
|
||||
uvicorn.run(app, host=settings.HOST, port=settings.PORT)
|
||||
|
||||
def init() -> None:
|
||||
uvicorn.run("barker.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init()
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, UniqueConstraint, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .regime import Regime
|
||||
from .voucher import Voucher
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Bill:
|
||||
__tablename__ = "bills"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("voucher_id", "regime_id"),
|
||||
UniqueConstraint("regime_id", "bill_number"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
regime_id: Mapped[int] = mapped_column(Integer, ForeignKey("regimes.id"), nullable=False)
|
||||
voucher_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("vouchers.id"), nullable=False)
|
||||
is_valid: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
bill_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
voucher: Mapped[Voucher] = relationship(back_populates="bills")
|
||||
regime: Mapped[Regime] = relationship(back_populates="bills")
|
||||
|
||||
def __init__(self, regime_id: int, is_valid: bool, bill_number: int):
|
||||
self.regime_id = regime_id
|
||||
self.is_valid = is_valid
|
||||
self.bill_number = bill_number
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Date, ForeignKey, Numeric, Uuid, func, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .stock_keeping_unit import StockKeepingUnit
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class BundleItem:
|
||||
__tablename__ = "bundle_items"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
|
||||
)
|
||||
bundle_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
|
||||
item_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
sale_price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
print_in_bill: Mapped[bool] = mapped_column(Boolean, server_default=text("true"), nullable=False)
|
||||
|
||||
valid_from: Mapped[date | None] = mapped_column(Date(), nullable=True)
|
||||
valid_till: Mapped[date | None] = mapped_column(Date(), nullable=True)
|
||||
|
||||
bundle: Mapped[StockKeepingUnit] = relationship(
|
||||
"StockKeepingUnit", back_populates="bundle_items", foreign_keys=[bundle_id]
|
||||
)
|
||||
item: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit", foreign_keys=[item_id])
|
||||
|
||||
__table_args__ = (
|
||||
postgresql.ExcludeConstraint(
|
||||
(bundle_id, "="),
|
||||
(item_id, "="),
|
||||
(func.daterange(valid_from, valid_till, text("'[]'")), "&&"),
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
quantity: Decimal,
|
||||
sale_price: Decimal,
|
||||
print_in_bill: bool,
|
||||
bundle_id: uuid.UUID,
|
||||
item_id: uuid.UUID,
|
||||
bundle: StockKeepingUnit | None = None,
|
||||
item: StockKeepingUnit | None = None,
|
||||
valid_from: date | None = None,
|
||||
valid_till: date | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.quantity = quantity
|
||||
self.sale_price = sale_price
|
||||
self.print_in_bill = print_in_bill
|
||||
self.valid_from = valid_from
|
||||
self.valid_till = valid_till
|
||||
if bundle_id is not None:
|
||||
self.bundle_id = bundle_id
|
||||
if item_id is not None:
|
||||
self.item_id = item_id
|
||||
if bundle is not None:
|
||||
self.bundle = bundle
|
||||
if item is not None:
|
||||
self.item = item
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
@@ -1,36 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Boolean, Column, Unicode, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, CheckConstraint, Text, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
class Customer(Base):
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .customer_discount import CustomerDiscount
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Customer:
|
||||
__tablename__ = "customers"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"length(trim(both from name)) >= 1",
|
||||
name="name_not_blank",
|
||||
),
|
||||
CheckConstraint(
|
||||
"length(trim(both from phone)) >= 1",
|
||||
name="phone_not_blank",
|
||||
),
|
||||
)
|
||||
name = Column("name", Unicode(255), nullable=False)
|
||||
phone = Column("phone", Unicode(255), nullable=False, unique=True)
|
||||
address = Column("address", Unicode(255), nullable=True)
|
||||
print_in_bill = Column("print_in_bill", Boolean, server_default=expression.false(), default=False, nullable=False)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
phone: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
discounts = relationship("CustomerDiscount", back_populates="customer")
|
||||
print_in_bill: Mapped[bool] = mapped_column(Boolean, server_default=expression.false(), nullable=False)
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
return self.name
|
||||
discounts: Mapped[list[CustomerDiscount]] = relationship(back_populates="customer")
|
||||
|
||||
def __init__(self, name=None, phone=None, address=None, print_in_bill=None, id_=None):
|
||||
def __init__(self, name: str, phone: str, address: str | None, print_in_bill: bool, id_: uuid.UUID | None = None):
|
||||
self.name = name
|
||||
self.phone = phone
|
||||
self.address = address
|
||||
self.print_in_bill = print_in_bill
|
||||
self.id = id_
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
|
||||
@classmethod
|
||||
def cash(cls):
|
||||
def cash(cls) -> uuid.UUID:
|
||||
return uuid.UUID("2c716f4b-0736-429a-ad51-610d7c47cb5e")
|
||||
@@ -1,33 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Column, ForeignKey, Numeric, UniqueConstraint, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import ForeignKey, Numeric, UniqueConstraint, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class CustomerDiscount(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .customer import Customer
|
||||
from .sale_category import SaleCategory
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class CustomerDiscount:
|
||||
__tablename__ = "customer_discount"
|
||||
__table_args__ = (UniqueConstraint("customer_id", "sale_category_id"),)
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
customer_id = Column("customer_id", UUID(as_uuid=True), ForeignKey("customers.id"), nullable=False)
|
||||
sale_category_id = Column("sale_category_id", UUID(as_uuid=True), ForeignKey("sale_categories.id"), nullable=False)
|
||||
discount = Column("discount", Numeric(precision=15, scale=5), nullable=False)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
customer_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("customers.id"), nullable=False)
|
||||
sale_category_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("sale_categories.id"), nullable=False)
|
||||
discount: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
|
||||
customer = relationship("Customer", back_populates="discounts")
|
||||
sale_category = relationship("SaleCategory")
|
||||
customer: Mapped[Customer] = relationship(back_populates="discounts")
|
||||
sale_category: Mapped[SaleCategory] = relationship("SaleCategory")
|
||||
|
||||
def __init__(self, sale_category_id=None, discount=None, id_=None, customer_id=None, customer=None):
|
||||
def __init__(
|
||||
self,
|
||||
sale_category_id: uuid.UUID,
|
||||
discount: Decimal,
|
||||
id_: uuid.UUID | None = None,
|
||||
customer_id: uuid.UUID | None = None,
|
||||
customer: Customer | None = None,
|
||||
):
|
||||
self.sale_category_id = sale_category_id
|
||||
self.discount = discount
|
||||
self.id = id_
|
||||
self.date = datetime.utcnow()
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
self.date = datetime.now(UTC).replace(tzinfo=None)
|
||||
if customer is None:
|
||||
self.customer_id = customer_id
|
||||
if customer_id is not None:
|
||||
self.customer_id = customer_id
|
||||
else:
|
||||
self.customer = customer
|
||||
@@ -1,20 +1,21 @@
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import JSON, Column, Unicode, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy import JSON, Text, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class DbSetting(Base):
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class DbSetting:
|
||||
__tablename__ = "settings"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
name = Column("name", Unicode(255), unique=True, nullable=False)
|
||||
data = Column("data", JSON)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
|
||||
data = mapped_column(JSON, nullable=False)
|
||||
|
||||
def __init__(self, id_=None, name=None, data=None):
|
||||
self.id = id_
|
||||
def __init__(self, id_: uuid.UUID | None, name: str, data: JSON):
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
self.name = name
|
||||
self.data = data
|
||||
@@ -1,39 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from barker.models.login_history import LoginHistory
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Unicode, desc, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Session, relationship
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Text, Uuid, desc, text
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
from .login_history import LoginHistory
|
||||
|
||||
|
||||
class Device(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .section import Section
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Device:
|
||||
__tablename__ = "devices"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
name = Column("name", Unicode(255), unique=True, nullable=False)
|
||||
enabled = Column("enabled", Boolean, nullable=False)
|
||||
section_id = Column("section_id", UUID(as_uuid=True), ForeignKey("sections.id"), nullable=False)
|
||||
creation_date = Column("creation_date", DateTime(), nullable=False)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
name: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
section_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("sections.id"), nullable=False)
|
||||
creation_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
|
||||
|
||||
section = relationship("Section", foreign_keys=section_id)
|
||||
login_history = relationship("LoginHistory", order_by=desc(LoginHistory.date), backref="device")
|
||||
section: Mapped[Section] = relationship(back_populates="devices")
|
||||
login_history: Mapped[list[LoginHistory]] = relationship(order_by=desc(LoginHistory.date), back_populates="device")
|
||||
|
||||
def __init__(self, name=None, enabled=None, section_id=None, creation_date=None, id_=None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
enabled: bool,
|
||||
section_id: uuid.UUID,
|
||||
creation_date: datetime | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.enabled = enabled
|
||||
self.section_id = section_id
|
||||
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
|
||||
self.id = id_
|
||||
self.creation_date = datetime.now(UTC).replace(tzinfo=None) if creation_date is None else creation_date
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
|
||||
@classmethod
|
||||
def create(cls, db: Session):
|
||||
def create(cls, db: Session) -> Device:
|
||||
main_section = uuid.UUID("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df")
|
||||
name = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
device = Device(name, False, main_section)
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, Unicode, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, Text, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class FoodTable(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .overview import Overview
|
||||
from .section import Section
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class FoodTable:
|
||||
__tablename__ = "food_tables"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
name = Column("name", Unicode(255), nullable=False, unique=True)
|
||||
seats = Column("seats", Integer, nullable=False)
|
||||
section_id = Column("section_id", UUID(as_uuid=True), ForeignKey("sections.id"), nullable=False)
|
||||
is_active = Column("is_active", Boolean, nullable=False)
|
||||
sort_order = Column("sort_order", Integer, nullable=False)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||
seats: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
section_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("sections.id"), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
section = relationship("Section", foreign_keys=section_id)
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
return self.name
|
||||
section: Mapped[Section] = relationship("Section", foreign_keys=section_id)
|
||||
status: Mapped[Overview | None] = relationship(back_populates="food_table", uselist=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name=None,
|
||||
seats=None,
|
||||
section_id=None,
|
||||
is_active=None,
|
||||
sort_order=0,
|
||||
id_=None,
|
||||
name: str,
|
||||
seats: int,
|
||||
section_id: uuid.UUID,
|
||||
is_active: bool,
|
||||
sort_order: int = 0,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.seats = seats
|
||||
self.section_id = section_id
|
||||
self.is_active = is_active
|
||||
self.sort_order = sort_order
|
||||
self.id = id_
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
@@ -1,30 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import (
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
Text,
|
||||
Uuid,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
from .guest_book_type import GuestBookType
|
||||
|
||||
|
||||
class GuestBook(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .customer import Customer
|
||||
from .overview import Overview
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class GuestBook:
|
||||
__tablename__ = "guest_book"
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
customer_id = Column("customer_id", UUID(as_uuid=True), ForeignKey("customers.id"), nullable=False)
|
||||
pax = Column("pax", Integer, nullable=False)
|
||||
date = Column("creation_date", DateTime(), nullable=False)
|
||||
__table_args__ = (CheckConstraint("num_nonnulls(booking_date, arrival_date) > 0", name="nulls"),)
|
||||
|
||||
customer = relationship("Customer")
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
customer_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("customers.id"), nullable=False)
|
||||
pax: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
booking_date: Mapped[datetime | None] = mapped_column(DateTime(), nullable=True)
|
||||
arrival_date: Mapped[datetime | None] = mapped_column(DateTime(), nullable=True)
|
||||
last_edit_date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
type_: Mapped[GuestBookType] = mapped_column("type", Enum(GuestBookType), nullable=False)
|
||||
|
||||
def __init__(self, pax=None, id_=None, customer_id=None, customer=None):
|
||||
self.customer_id = customer_id
|
||||
self.pax = pax
|
||||
self.id = id_
|
||||
self.date = datetime.utcnow()
|
||||
if customer is None:
|
||||
customer: Mapped[Customer] = relationship("Customer")
|
||||
status: Mapped[Overview | None] = relationship(back_populates="guest", uselist=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pax: int,
|
||||
booking_date: datetime | None,
|
||||
arrival_date: datetime | None,
|
||||
notes: str | None,
|
||||
type_: GuestBookType,
|
||||
customer_id: uuid.UUID | None = None,
|
||||
customer: Customer | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
if customer_id is not None:
|
||||
self.customer_id = customer_id
|
||||
else:
|
||||
self.pax = pax
|
||||
if booking_date is None and arrival_date is None:
|
||||
raise Exception("Both arrival and booking date cannot be null")
|
||||
|
||||
self.booking_date = booking_date
|
||||
self.arrival_date = arrival_date
|
||||
self.last_edit_date = datetime.now(UTC).replace(tzinfo=None)
|
||||
self.notes = notes
|
||||
self.type_ = type_
|
||||
if customer is None and customer_id is not None:
|
||||
self.customer_id = customer_id
|
||||
if customer is not None:
|
||||
self.customer = customer
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
@@ -0,0 +1,10 @@
|
||||
import enum
|
||||
|
||||
|
||||
class GuestBookType(enum.StrEnum):
|
||||
running = "running"
|
||||
printed = "printed"
|
||||
walk_in = "walk_in"
|
||||
booking = "booking"
|
||||
arrived = "arrived"
|
||||
old = "old"
|
||||
@@ -1,63 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from barker.models.meta import Base
|
||||
from barker.models.product import Product
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
ColumnElement,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
Numeric,
|
||||
UniqueConstraint,
|
||||
Uuid,
|
||||
case,
|
||||
func,
|
||||
text,
|
||||
type_coerce,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
from .inventory_type import InventoryType
|
||||
from .tax import Tax
|
||||
|
||||
|
||||
class Inventory(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .inventory_modifier import InventoryModifier
|
||||
from .kot import Kot
|
||||
from .stock_keeping_unit import StockKeepingUnit
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Inventory:
|
||||
__tablename__ = "inventories"
|
||||
__table_args__ = (UniqueConstraint("kot_id", "product_id", "is_happy_hour", "price"),)
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
__table_args__ = (
|
||||
UniqueConstraint("kot_id", "sku_id", "is_happy_hour", "type", "parent_id", postgresql_nulls_not_distinct=True),
|
||||
)
|
||||
kot_id = Column("kot_id", UUID(as_uuid=True), ForeignKey("kots.id"), nullable=False, index=True)
|
||||
product_id = Column("product_id", UUID(as_uuid=True), ForeignKey("products.id"), nullable=False)
|
||||
quantity = Column("quantity", Numeric(precision=15, scale=2))
|
||||
price = Column("price", Numeric(precision=15, scale=2))
|
||||
is_happy_hour = Column("is_happy_hour", Boolean, nullable=False)
|
||||
tax_rate = Column("tax_rate", Numeric(precision=15, scale=5))
|
||||
tax_id = Column("tax_id", UUID(as_uuid=True), ForeignKey("taxes.id"), nullable=False)
|
||||
discount = Column("discount", Numeric(precision=15, scale=5))
|
||||
sort_order = Column("sort_order", Integer, nullable=False)
|
||||
|
||||
kot = relationship("Kot", back_populates="inventories")
|
||||
tax = relationship("Tax", foreign_keys=tax_id)
|
||||
product = relationship("ProductVersion", secondary=Product.__table__, back_populates="inventories", viewonly=True)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
kot_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("kots.id"), nullable=False, index=True)
|
||||
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
is_happy_hour: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
tax_rate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
tax_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("taxes.id"), nullable=False)
|
||||
discount: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
type_: Mapped[InventoryType] = mapped_column(
|
||||
"type", Enum(InventoryType), server_default=text("regular"), nullable=False
|
||||
)
|
||||
parent_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=True, index=True)
|
||||
|
||||
kot: Mapped[Kot] = relationship(back_populates="inventories")
|
||||
tax: Mapped[Tax] = relationship(back_populates="inventories")
|
||||
sku: Mapped[StockKeepingUnit] = relationship(back_populates="inventories")
|
||||
parent: Mapped[Inventory | None] = relationship("Inventory", remote_side="Inventory.id", back_populates="children")
|
||||
children: Mapped[list[Inventory]] = relationship("Inventory", back_populates="parent")
|
||||
|
||||
modifiers: Mapped[list[InventoryModifier]] = relationship(back_populates="inventory")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
kot_id,
|
||||
product_id,
|
||||
quantity,
|
||||
price,
|
||||
discount,
|
||||
is_hh,
|
||||
tax_id,
|
||||
tax_rate,
|
||||
sort_order,
|
||||
product=None,
|
||||
tax=None,
|
||||
kot_id: uuid.UUID | None = None,
|
||||
kot: Kot | None = None,
|
||||
sku_id: uuid.UUID | None = None,
|
||||
quantity: Decimal = Decimal(0),
|
||||
price: Decimal = Decimal(0),
|
||||
discount: Decimal = Decimal(0),
|
||||
is_hh: bool = False,
|
||||
tax_id: uuid.UUID | None = None,
|
||||
tax_rate: Decimal = Decimal(0),
|
||||
sort_order: int = 0,
|
||||
type_: InventoryType = InventoryType.regular,
|
||||
parent_id: uuid.UUID | None = None,
|
||||
parent: Inventory | None = None,
|
||||
sku: StockKeepingUnit | None = None,
|
||||
tax: Tax | None = None,
|
||||
):
|
||||
self.kot_id = kot_id
|
||||
if product_id is not None:
|
||||
self.product_id = product_id
|
||||
if kot_id is not None:
|
||||
self.kot_id = kot_id
|
||||
if kot is not None:
|
||||
self.kot = kot
|
||||
if sku_id is not None:
|
||||
self.sku_id = sku_id
|
||||
self.quantity = quantity
|
||||
self.price = price
|
||||
self.discount = discount
|
||||
@@ -66,39 +95,56 @@ class Inventory(Base):
|
||||
self.tax_id = tax_id
|
||||
self.tax_rate = tax_rate
|
||||
self.sort_order = sort_order
|
||||
if product is not None:
|
||||
self.product = product
|
||||
self.type_ = type_
|
||||
if parent_id is not None:
|
||||
self.parent_id = parent_id
|
||||
if parent is not None:
|
||||
self.parent = parent
|
||||
if sku is not None:
|
||||
self.sku = sku
|
||||
if tax is not None:
|
||||
self.tax = tax
|
||||
|
||||
@hybrid_property
|
||||
def effective_price(self):
|
||||
return 0 if self.is_happy_hour else self.price
|
||||
def effective_price(self) -> Decimal:
|
||||
return Decimal(0) if self.is_happy_hour else self.price
|
||||
|
||||
@effective_price.expression
|
||||
def effective_price(cls):
|
||||
return case([(cls.is_happy_hour == True, 0)], else_=cls.price) # noqa: E712
|
||||
@effective_price.inplace.expression
|
||||
@classmethod
|
||||
def _effective_price_expression(cls) -> ColumnElement[Decimal]:
|
||||
return type_coerce(
|
||||
case((cls.is_happy_hour == True, 0), else_=cls.price), # noqa: E712
|
||||
Numeric(precision=15, scale=2),
|
||||
).label("effective_price")
|
||||
|
||||
@hybrid_property
|
||||
def net(self):
|
||||
def net(self) -> Decimal:
|
||||
return self.effective_price * self.quantity * (1 - self.discount)
|
||||
|
||||
@net.expression
|
||||
def net(cls):
|
||||
return cls.effective_price * cls.quantity * (1 - cls.discount)
|
||||
@net.inplace.expression
|
||||
@classmethod
|
||||
def _net_expression(cls) -> ColumnElement[Decimal]:
|
||||
return type_coerce(
|
||||
cls.effective_price * cls.quantity * (1 - cls.discount), Numeric(precision=15, scale=2)
|
||||
).label("net")
|
||||
|
||||
@hybrid_property
|
||||
def tax_amount(self):
|
||||
def tax_amount(self) -> Decimal:
|
||||
return self.net * self.tax_rate
|
||||
|
||||
@tax_amount.expression
|
||||
def tax_amount(cls):
|
||||
return cls.net * cls.tax_rate
|
||||
@tax_amount.inplace.expression
|
||||
@classmethod
|
||||
def _tax_amount_expression(cls) -> ColumnElement[Decimal]:
|
||||
return type_coerce(cls.net * cls.tax_rate, Numeric(precision=15, scale=2)).label("tax_amount")
|
||||
|
||||
@hybrid_property
|
||||
def amount(self):
|
||||
def amount(self) -> Decimal:
|
||||
return round(Decimal(self.net * (1 + self.tax_rate)), 2)
|
||||
|
||||
@amount.expression
|
||||
def amount(cls):
|
||||
return func.round(cls.net * (1 + cls.tax_rate), 2)
|
||||
@amount.inplace.expression
|
||||
@classmethod
|
||||
def _amount_expression(cls) -> ColumnElement[Decimal]:
|
||||
# func.round(cls.net * (1 + cls.tax_rate), 2)
|
||||
return type_coerce(
|
||||
func.round(cls.net * (1 + cls.tax_rate) / 0.02, 0) * 0.02, Numeric(precision=15, scale=2)
|
||||
).label("amount")
|
||||
@@ -1,26 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Column, ForeignKey, Numeric, UniqueConstraint, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Numeric, UniqueConstraint, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class InventoryModifier(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .inventory import Inventory
|
||||
from .modifier import Modifier
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class InventoryModifier:
|
||||
__tablename__ = "inventory_modifiers"
|
||||
__table_args__ = (UniqueConstraint("inventory_id", "modifier_id"),)
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
inventory_id = Column("inventory_id", UUID(as_uuid=True), ForeignKey("inventories.id"), nullable=False)
|
||||
modifier_id = Column("modifier_id", UUID(as_uuid=True), ForeignKey("modifiers.id"), nullable=False)
|
||||
price = Column("price", Numeric(precision=15, scale=2), nullable=False)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
inventory_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("inventories.id"), nullable=False, index=True)
|
||||
modifier_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("modifiers.id"), nullable=False)
|
||||
price: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False)
|
||||
|
||||
inventory = relationship("Inventory", backref="modifiers")
|
||||
modifier = relationship("Modifier")
|
||||
inventory: Mapped[Inventory] = relationship(back_populates="modifiers")
|
||||
modifier: Mapped[Modifier] = relationship("Modifier")
|
||||
|
||||
def __init__(self, inventory_id, modifier_id, price):
|
||||
self.inventory_id = inventory_id
|
||||
self.modifier_id = modifier_id
|
||||
def __init__(self, inventory_id: uuid.UUID | None, modifier_id: uuid.UUID | None, price: Decimal):
|
||||
if inventory_id is not None:
|
||||
self.inventory_id = inventory_id
|
||||
if modifier_id is not None:
|
||||
self.modifier_id = modifier_id
|
||||
self.price = price
|
||||
@@ -0,0 +1,7 @@
|
||||
import enum
|
||||
|
||||
|
||||
class InventoryType(enum.StrEnum):
|
||||
regular = "regular"
|
||||
bundle = "bundle"
|
||||
bundle_item = "bundle_item"
|
||||
+38
-27
@@ -1,48 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class Kot(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .food_table import FoodTable
|
||||
from .inventory import Inventory
|
||||
from .user import User
|
||||
from .voucher import Voucher
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Kot:
|
||||
__tablename__ = "kots"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
voucher_id = Column(
|
||||
"voucher_id",
|
||||
UUID(as_uuid=True),
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
voucher_id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid,
|
||||
ForeignKey("vouchers.id"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
code = Column("code", Integer, nullable=False, unique=True)
|
||||
food_table_id = Column(
|
||||
"food_table_id",
|
||||
UUID(as_uuid=True),
|
||||
code: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
|
||||
food_table_id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid,
|
||||
ForeignKey("food_tables.id"),
|
||||
nullable=False,
|
||||
)
|
||||
date = Column("date", DateTime, nullable=False, index=True)
|
||||
user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
date: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
user = relationship("User", backref="kots")
|
||||
food_table = relationship("FoodTable", backref="kots")
|
||||
inventories = relationship("Inventory", back_populates="kot")
|
||||
voucher: Mapped[Voucher] = relationship(back_populates="kots")
|
||||
user: Mapped[User] = relationship(backref="kots")
|
||||
food_table: Mapped[FoodTable] = relationship(backref="kots")
|
||||
inventories: Mapped[list[Inventory]] = relationship(back_populates="kot")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
voucher_id=None,
|
||||
code=None,
|
||||
food_table_id=None,
|
||||
date=None,
|
||||
user_id=None,
|
||||
id_=None,
|
||||
voucher_id: uuid.UUID,
|
||||
code: int,
|
||||
food_table_id: uuid.UUID,
|
||||
date: datetime,
|
||||
user_id: uuid.UUID,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.id = id_
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
self.voucher_id = voucher_id
|
||||
self.code = code
|
||||
self.food_table_id = food_table_id
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, UniqueConstraint, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class LoginHistory(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .device import Device
|
||||
from .user import User
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class LoginHistory:
|
||||
__tablename__ = "login_history"
|
||||
__table_args__ = (UniqueConstraint("user_id", "device_id", "date"),)
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
device_id = Column(
|
||||
"device_id",
|
||||
UUID(as_uuid=True),
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("users.id"), nullable=False)
|
||||
device_id: Mapped[uuid.UUID] = mapped_column(
|
||||
Uuid,
|
||||
ForeignKey("devices.id"),
|
||||
nullable=False,
|
||||
)
|
||||
date = Column("date", DateTime(), nullable=False)
|
||||
date: Mapped[datetime] = mapped_column(DateTime(), nullable=False)
|
||||
|
||||
def __init__(self, user_id=None, device_id=None, date=None, id_=None):
|
||||
device: Mapped[Device] = relationship(back_populates="login_history")
|
||||
user: Mapped[User] = relationship(back_populates="login_history")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
device_id: uuid.UUID,
|
||||
date: datetime | None = None,
|
||||
id_: uuid.UUID | None = None,
|
||||
):
|
||||
self.user_id = user_id
|
||||
self.device_id = device_id
|
||||
self.date = datetime.utcnow() if date is None else date
|
||||
self.id = id_
|
||||
self.date = datetime.now(UTC).replace(tzinfo=None) if date is None else date
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
@@ -1,25 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Boolean, Column, Integer, Unicode, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Integer, Text, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class MenuCategory(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .sku_version import SkuVersion
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class MenuCategory:
|
||||
__tablename__ = "menu_categories"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
name = Column("name", Unicode(255), nullable=False, unique=True)
|
||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, server_default=text("gen_random_uuid()"))
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||
|
||||
is_active = Column("is_active", Boolean, nullable=False)
|
||||
is_fixture = Column("is_fixture", Boolean, nullable=False)
|
||||
sort_order = Column("sort_order", Integer, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
is_fixture: Mapped[bool] = mapped_column(Boolean, nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
def __init__(self, name, is_active, sort_order, is_fixture=False, id_=None):
|
||||
skus: Mapped[list[SkuVersion]] = relationship(back_populates="menu_category")
|
||||
|
||||
def __init__(
|
||||
self, name: str, is_active: bool, sort_order: int, is_fixture: bool = False, id_: uuid.UUID | None = None
|
||||
):
|
||||
self.name = name
|
||||
self.is_active = is_active
|
||||
self.sort_order = sort_order
|
||||
self.is_fixture = is_fixture
|
||||
self.id = id_
|
||||
if id_ is not None:
|
||||
self.id = id_
|
||||
@@ -1,17 +0,0 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.schema import MetaData
|
||||
|
||||
|
||||
# Recommended naming convention used by Alembic, as various different database
|
||||
# providers will autogenerate vastly different names making migrations more
|
||||
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
|
||||
NAMING_CONVENTION = {
|
||||
"ix": "ix_%(column_0_label)s",
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s",
|
||||
}
|
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION)
|
||||
Base = declarative_base(metadata=metadata)
|
||||
@@ -1,40 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from barker.models.meta import Base
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Numeric, Unicode, text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Numeric, Text, Uuid, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from ..db.base_class import reg
|
||||
|
||||
|
||||
class Modifier(Base):
|
||||
if TYPE_CHECKING:
|
||||
from .modifier_category import ModifierCategory
|
||||
|
||||
|
||||
@reg.mapped_as_dataclass(unsafe_hash=True)
|
||||
class Modifier:
|
||||
__tablename__ = "modifiers"
|
||||
|
||||
id = Column(
|
||||
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), default=uuid.uuid4
|
||||
)
|
||||
name = Column("name", Unicode(255), nullable=False, unique=True)
|
||||
show_in_bill = Column("show_in_bill", Boolean, nullable=False)
|
||||
price = Column("price", Numeric(precision=15, scale=2), nullable=False)
|
||||
is_active = Column("is_active", Boolean, nullable=False)
|
||||
modifier_category_id = Column(
|
||||
"modifier_category_id",< | ||||