Compare commits

...
220 Commits
Author SHA1 Message Date
tanshu 2dfbfd8fcb Version Bump v15.1.6 2026-09-04 15:29:38 +05:30
tanshu 221fadfdfc Fix: Select customer could not edit or create a customer. 2026-09-04 09:59:29 +00:00
tanshu 11b93cb11c Version Bump v15.1.5 2026-09-04 15:17:50 +05:30
tanshu 302a4eb311 Fix: Receive payment refactored. Earlier it would refresh on input. 2026-09-04 09:47:42 +00:00
tanshu 217f636573 Version Bump v15.1.4 2026-09-04 13:44:05 +05:30
tanshu 95ac73bbc4 Updated tracking in template and choose customer fixing. 2026-09-04 08:13:26 +00:00
tanshu 3d86f8706f Version Bump v15.1.3 2026-09-04 09:03:38 +05:30
tanshu 30cbb60efe Choose customer would refresh while fetching customers. 2026-09-04 03:33:17 +00:00
tanshu 227f5b5f74 Version Bump v15.1.2 2026-09-02 18:38:08 +05:30
tanshu 50974c7574 Added a role for printer so that the container can depend on it. 2026-09-02 13:07:50 +00:00
tanshu c8e1ab74b6 Version Bump v15.1.1 2026-09-02 18:36:55 +05:30
tanshu 85ba7b326a Fix: Edge cases where backend and frontend totals are off and you cannot settle the bill. 2026-09-02 13:06:34 +00:00
tanshu 458dfaad50 Version Bump v15.1.0 2026-09-02 09:10:23 +05:30
tanshu 0a9675f270 We have moved from dd-MMM-yyyy to ISO Date. 2026-09-02 03:38:49 +00:00
tanshu 6cf42d3641 Added a handler to send the client errors to the backend. Updated the launch settings to reload the fastapi app on file changes. 2026-09-02 02:35:53 +00:00
tanshu cf680cd254 Version Bump v15.0.4 2026-09-01 07:45:23 +05:30
tanshu 80d21ec31e Minor fixes. Logs should now show the real ip address not the caddy container ip. 2026-09-01 02:15:01 +00:00
tanshu 4377016cda Version Bump v15.0.3 2026-08-31 12:11:38 +05:30
tanshu 67209886b9 current trixie does not have yarn 2026-08-31 06:41:31 +00:00
tanshu 4e1c0d1ec7 Version Bump v15.0.2 2026-08-31 10:02:01 +05:30
tanshu c7412f080d Fix: Newer trixie images did not enable yarn. 2026-08-31 04:31:48 +00:00
tanshu d023089720 Version Bump v15.0.1 2026-08-31 09:55:05 +05:30
tanshu dd02a97ba2 Forgot to add the updated uv.lock 2026-08-31 04:25:00 +00:00
tanshu befd4eb298 Version Bump v15.0.0 2026-08-31 04:23:12 +00:00
tanshu d90c1cb563 Moved from moment to luxon.
Report Printing is not a post not a get.
2026-08-31 04:19:05 +00:00
tanshu e90c80999e Frank now working with netbird and nats + jetstream 2026-08-30 15:57:59 +00:00
tanshu 42331b482d ansible improvements 2026-08-30 14:56:38 +00:00
tanshu 612e2243c0 Working on frand deployement 2026-08-30 10:37:09 +00:00
tanshu 35a3bb9ad2 netbird server works now. linted the yml files 2026-08-30 07:35:08 +00:00
tanshu ae69c2fafe Frank refactoring part 1 2026-08-30 04:41:27 +00:00
tanshu 85a063c1a7 fdsa 2026-08-29 21:09:42 +00:00
tanshu 10448e8b12 fds 2026-08-29 12:19:14 +00:00
tanshu 59bf2c5ca9 dfsa 2026-08-29 10:52:44 +00:00
tanshu 73fba17711 fdsa 2026-08-29 10:49:36 +00:00
tanshu 103983846d fasd 2026-08-28 15:44:19 +00:00
tanshu af07bb5043 fdsa 2026-08-27 12:08:58 +00:00
tanshu 612426b92f fasd 2026-08-27 11:56:52 +00:00
tanshu 730048f239 fda 2026-08-27 11:52:55 +00:00
tanshu ff537edb06 fds 2026-08-27 11:38:38 +00:00
tanshu 311464e593 csdxz 2026-08-27 06:06:30 +00:00
tanshu 43204e8441 fad 2026-08-27 05:38:32 +00:00
tanshu 2a29dc9195 cdsa 2026-08-18 13:13:11 +00:00
tanshu 65527dcbff gdfasg 2026-08-18 12:47:58 +00:00
tanshu cc52670783 mid 2026-08-18 12:28:51 +00:00
tanshu 993f83c786 toSignal via Gemini 2026-08-18 07:55:54 +00:00
tanshu 59eb45da96 Version Bump v14.5.0 2026-08-10 14:04:26 +05:30
tanshu ebe5a6352e Made the version bump more robust. 2026-08-10 08:33:15 +00:00
tanshu 38a8656008 Added proper agents config for mcp servers and skills.
Added the menu category delete route.
Fix: Show blank vouchers in tables
Fix: In rare cases, the old settements can stick around. fixed it.
2026-08-10 08:25:12 +00:00
tanshu 2e5543a2eb Optimizing and updating Dockerfile for build testing, size and cache. 2026-02-22 05:07:43 +00:00
tanshu 34e30efcaf Chore: Ruff linting updated and fixed errors.
Chore: Moved from poetry to uv
2026-02-22 04:23:14 +00:00
tanshu 2b844bc44b Chore: Using FastAPI Exception handlers for Integrity Errors and SQLAlchemyErrors.
Chore: Using Annotation to inject Session in routes.
Hence, have removed all with Session and try / catch
2026-02-21 08:26:29 +00:00
tanshu 325bcae998 Version Bump v14.3.1 2026-02-20 21:58:40 +05:30
tanshu ab8fed17d0 Fix: Product update failed as sku_version_id was not sent on the schema and hence update tried to create a new sku.
Fix: If a product sku was updated and then deleted on the same day, it failed as the valid_till for the sku_version was set to yesterday while the valid_from was set to today.
Fix: Product update also failed as the db.commit was on the loop adding new skus
2026-02-20 16:27:14 +00:00
tanshu dcf1ffad98 Version Bump v14.3.0 2026-02-17 09:45:32 +05:30
tanshu 338d9d63d5 Validation: Making Product name unique ignoring the fraction_units.
Making the sku_version.units unique for a product across stock_keeping_units.

Both respecting the validity
2026-02-17 04:13:11 +00:00
tanshu 9b0da9cb65 Fix: Product sale report was aggregating on the product_version_id 2026-02-14 12:20:09 +00:00
tanshu 4cc2ff2229 Version Bump v14.2.1 2026-02-14 11:30:40 +05:30
tanshu c9fa83e8ac Fix: Accidentally used menu category instead of sale category 2026-02-14 06:00:09 +00:00
tanshu 061dd1310d Version Bump v14.2.0 2026-02-14 05:30:05 +00:00
tanshu a1d6919d9d Beer consumption report can choose menu category for beer 2026-02-14 05:29:11 +00:00
tanshu ecc92795e6 Fix: In product list, if menu category is reset, all products get filtered out as it becomes undefined. 2026-02-12 09:01:12 +00:00
tanshu 535a145742 Fix: Cannot move kot as row.kotId was always undefined. 2026-02-12 08:20:54 +00:00
tanshu 1271e26dd8 Changed Docker CMD to remove redundant run.sh 2026-02-12 04:01:35 +00:00
tanshu 2ae3f3ba7e Dockerfile will kill the process if stuck so that restart policy can restart the container
Gunicorn will kill worker every 10000 requests which can help reclaim memory lost to leaks
2026-02-12 03:52:53 +00:00
tanshu e6a03d33e5 Version Bump v14.1.0 2026-02-11 14:59:50 +05:30
tanshu c7dd6e574d Feature: Roles can include Roles. 2026-02-11 09:28:50 +00:00
tanshu 913820cc29 Dockerfile improvements. 2026-02-11 08:41:12 +00:00
tanshu e3d40d50f0 Fix: Unable to receive payment as the amountVal as always zero. 2026-02-11 08:37:12 +00:00
tanshu b27a5a9211 Committed the poetry.lock 2026-02-11 05:51:10 +00:00
tanshu a955e26b93 Added poetry lockfile to Dockerfile 2026-02-11 05:48:41 +00:00
tanshu bd2b1f5c2f Version Bump v14.0.1 2026-02-11 10:53:26 +05:30
tanshu 0fa28019ec Fix: Accidentally removed the static files route needed to serve the frontend in deployment 2026-02-11 05:23:08 +00:00
tanshu ddb03ac8eb Version Bump v14.0.0 2026-02-11 10:43:43 +05:30
tanshu ae8975b29f Merge branch 'feature/bundle' 2026-02-11 10:42:01 +05:30
tanshu e7e49019c4 Schemas everywhere mypy and ruff fully compliant
Type hints everywhere
2026-02-11 04:41:22 +00:00
tanshu dc34a78e06 Cleanup 2026-02-10 18:12:52 +00:00
tanshu 91b3740ec0 Bundle item has an option to not print in the bill.
Bills and Kots should print properly with bundles
2026-02-10 18:00:03 +00:00
tanshu 7382d487ac Product Sale Report double counts the Bundle Items as it lists them as well as the bundle.
Menu Engineering Report double counts the Bundle Items as it lists them as well as the bundle.

All reports fixed.
2026-02-05 01:12:56 +00:00
tanshu 8957e4ce8c All voucher routes now work with bundles.
Need to check / update all reports and prints
2026-02-05 00:46:49 +00:00
tanshu 6e8843d3c4 Feature: Added product bundles. Have to wire it up to inventory and reports. 2026-01-31 15:16:58 +00:00
tanshu b79af6e1c6 Changed the for-product route to for-sku to reduce ambiguity 2026-01-29 16:23:07 +00:00
tanshu c65517b377 Fix 2026-01-28 07:18:10 +00:00
tanshu ca94f8f57e Upgraded to Angular v21
Removed tailwind

Moved to vitest from karma/jasmine
2026-01-28 07:18:05 +00:00
tanshu e0766c5a2e Merge branch 'sku' 2026-01-28 04:16:04 +00:00
tanshu bd1a5e03c5 Merge branch 'main' into sku 2026-01-28 09:39:59 +05:30
tanshu 61f5f2f1af Product update sort order working
Beer Sale report should work. But it has all the products in it now.
All reports working
2026-01-28 04:07:22 +00:00
tanshu 0a7ffb4a5c Product list and detail fully working. only thing to check is the update sort order route 2026-01-26 14:01:18 +00:00
tanshu 22f888500f Update product prices now working 2026-01-26 14:00:14 +00:00
tanshu 2505ef7aec Temporal Product Fixed. 2026-01-26 07:07:33 +00:00
tanshu 4f0346551a Modifier Category Working 2026-01-24 03:26:03 +00:00
tanshu e4072d0872 Sort order is no longer versioned. Also, data moved is not normalized to add the right skus to the products and coalesce contiguous products and skus with no changes. 2026-01-23 16:20:08 +00:00
tanshu 1b8ab2857f Mostly working with Product + Version and Sku + Version.
There will still be many errors. But this is working somewhat
2026-01-23 02:30:57 +00:00
tanshu aaf64ab75a Docker health check 2026-01-15 09:25:38 +05:30
tanshu 6caeb34e8d Change: The cashier report will now split the summary and details. 2025-09-09 07:14:48 +05:30
tanshu 88b53b5781 Change: The cashier report will now split the summary and details. 2025-09-09 01:40:07 +00:00
tanshu 8ff7363fc8 Fixed: VoucherType as not number. So, if a bill was being printed directly without kot first being printed, it would not ask for bill type as the comparison was comparing string with int. 2025-08-09 17:19:30 +00:00
tanshu e1cbf7ffb8 Trackby for the bill table. This should prevent rerendering and it jumping up. 2025-08-04 15:27:21 +00:00
tanshu b33468a43f Fix: Show voucher without kots and iventories would cause an error in show. 2025-08-04 14:54:42 +00:00
tanshu 3e8f2e9736 Changed the font 2025-07-29 09:04:03 +00:00
tanshu f2d5d62aa6 Fix: Beer Sale Report 2025-07-17 17:13:35 +00:00
tanshu eee9ae1f11 CSS Class updation 2025-07-17 03:29:14 +00:00
tanshu 2fbfe96c40 Fix: Due to matching of timestamp vs date, on the day that a product valid_till expired,
comparison with voucher.date would wrongly fail.
2025-07-17 03:28:22 +00:00
tanshu 01b0cbfadf Removed Unnecessary Flex class 2025-07-11 11:44:28 +00:00
tanshu 88a1572747 Chore: Removed Mat Card from everywhere.
Chore: Removed the Toaster Service.
Chore: Updated to Material Theme 3
2025-07-11 11:30:35 +00:00
tanshu 5c1c474e93 Fix: Product Sale Report was not showing "H H " and also printing was an issue. 2025-07-10 16:05:56 +00:00
tanshu 74e17dffd0 Version Bump v13.1.0 2025-07-10 10:59:03 +00:00
tanshu 8ad9e6562c Removed all mr-5, mb-5 and basis tailwind classes.
Will soon remove tailwind totally.
2025-07-10 10:58:48 +00:00
tanshu d80d097c0e Fix: Sale category was not opening as product query error of sorting on menu category without a join 2025-07-10 10:56:30 +00:00
tanshu 0dbec4784b Feature: Paged customer list. 2025-07-10 09:39:55 +00:00
tanshu af684c976e Version Bump v13.0.1 2025-07-09 15:37:37 +00:00
tanshu 26c65b4b65 Fix: Could not open bill with bill number or voucher id 2025-07-09 15:35:35 +00:00
tanshu 2c89e218c8 Version Bump v13.0.0 2025-07-09 14:14:01 +00:00
tanshu b2732434f4 Fix: Kots were not sorted in show voucher. 2025-07-09 14:10:31 +00:00
tanshu 5317e3de23 Fix: Vouchertype as int not string. This fucked up everthing in the frontend and was not able to print kot or bills. 2025-07-09 06:37:31 +00:00
tanshu add3ad35d7 Fix: Missing report schemas 2025-07-09 05:05:30 +00:00
tanshu feb867fe28 Fix: Ansible 2025-07-09 05:04:19 +00:00
tanshu fc2ef36a0c Update for new deployment with Caddy and docker networks 2025-07-09 03:46:33 +00:00
tanshu 31b4ee1ebf Chore: fixed a lot of mypy errors. 2025-07-03 06:27:10 +00:00
tanshu 0e9d9c5934 In the following using onclause for joining ProductVersion vs where:
Product Router
Beer Sale Report
Discount Report
Menu Engineering Report
Product Sale Report
Product Updates Report
Sale Report


Create a pydantic schema for show voucher

Fixed modifier dialog in sales to original size.
2025-07-02 17:44:39 +00:00
tanshu 5401318fc1 Serving method changed to caddy 2025-07-02 04:48:06 +00:00
tanshu 66834b7399 Fix: Modifier Category was showing products from out of category. 2025-07-02 04:34:13 +00:00
tanshu 44513dd6be Moved to Angular 20
Moved to Tailwind 4
Moved to Python 3.13
Enabled arm64/v8 Builds
2025-07-02 04:32:35 +00:00
tanshu 86722e3558 Version Bump v12.1.8 2025-05-27 13:58:48 +00:00
tanshu 2ca94e5bea Add tax was not working 2025-05-27 13:57:46 +00:00
tanshu ad8ccb26df Version Bump v12.1.7 2025-05-02 02:33:32 +00:00
tanshu abc1b36721 Auth errors were not showing up as they were swallowed up by the auth interceptor.
Once caught, the device name was not being shown as it was triggering an expression changed after detection error.
2025-05-02 02:02:25 +00:00
tanshu 68ab90ec48 Chore: Cleaned up imports to use Modules.
Feature: Added an eslint rule to sort component imports
2024-12-19 08:29:05 +05:30
tanshu 7be7545137 Version Bump v12.1.6 2024-12-17 22:26:50 +05:30
tanshu b46fd10f5a Modifier was not working as when the data came late, it would display blank. 2024-12-17 22:26:35 +05:30
tanshu 50846ace3f Version Bump v12.1.5 2024-12-17 21:21:31 +05:30
tanshu e36de39ed5 Modifiers issues 2024-12-17 21:21:21 +05:30
tanshu 4b1fca0260 Version Bump v12.1.4 2024-12-17 09:47:34 +05:30
tanshu 43a884144b Fix: Issues flagged by mypy 2024-12-17 09:47:25 +05:30
tanshu 85a939b761 Fix: Show section wise settlement and tax 2024-12-17 09:46:44 +05:30
tanshu 3bfa00364c Version Bump v12.1.3 2024-12-17 09:18:43 +05:30
tanshu 0b092b5467 Fix: login failed as python-jose was removed. 2024-12-17 09:18:33 +05:30
tanshu 82747c4b89 Version Bump v12.1.2 2024-12-17 09:04:56 +05:30
tanshu e732354671 Fix: run was referencing files in a subdirectory.
Fix: Playbook was not importing the default vars.
Improvement: Dockerfile casheing should improve.
2024-12-17 09:04:49 +05:30
tanshu 6d3057bb55 Version Bump v12.1.1 2024-12-17 08:58:54 +05:30
tanshu 15a93c599e Chore: ruff check will fix the errors also. 2024-12-17 08:58:45 +05:30
tanshu 823e3a761e Fix: Docker entrypoint command. run.sh is in the /app subdirectory 2024-12-17 08:58:17 +05:30
tanshu ac728128b9 Chore: Cleanup Module imports 2024-12-17 08:57:52 +05:30
tanshu 5536bcae41 Version Bump v12.1.0 2024-12-17 08:47:13 +05:30
tanshu 55d5017254 Product sale report can now be section wise 2024-12-17 08:46:29 +05:30
tanshu bbb7be8070 Sale report now can be section wise 2024-12-17 08:23:18 +05:30
tanshu b1407f339d Version Bump v12.0.1 2024-12-16 21:18:26 +05:30
tanshu 659fed549a Fix: Discount and payment received were not working 2024-12-16 21:17:58 +05:30
tanshu a5efcc9061 Fix: Dockerfile was erroring out. 2024-12-16 19:03:38 +05:30
tanshu 7e70214c94 Version Bump v12.0.0 2024-12-16 17:58:20 +05:30
tanshu 2495c24e1a Chore: Updated python dependencies
Chore: Updated angular to v19
Chore: Refactored ops with docker and ansible
2024-12-16 17:58:17 +05:30
tanshu 010e9a84db Chore: Upgrade to Angular v18
Chore: Upgrade to Python 3.12
Chore: Upgrade to psycopg3
2024-06-03 13:22:56 +05:30
tanshu 56c1be5e05 Chore: Moved from isort + flake8 to ruff. There are a few formatting differences in isort and ruff
Chore: Updated dependencies
2023-08-31 07:16:58 +05:30
tanshu cbd5da12cc Version Bump v11.7.0 2023-08-28 11:03:22 +05:30
tanshu 93b1b8840d Feature: Updated the frank dockerfile to properly install python poetry
Feature: Updated the frank playbook to allow user to be changed from pi
Chore: Refactored the various env files to use single file and multiple var files.
2023-08-28 11:03:00 +05:30
tanshu ecea277e46 Feature: Table Booking in guestbook.
Feature: Guest book list shows the running cover count
2023-08-28 07:14:24 +05:30
tanshu 1cb2677ad1 Chore: Moved edit and delete in guestbook to the details from list.
This will give more space.
Also removed the old table button
2023-08-17 22:18:14 +05:30
tanshu e8a33cd80e Chore: Updated bake pies to current services 2023-08-17 22:17:09 +05:30
tanshu cb3657615a Chore: Updated fastapi and pydantic
Refactor: Removed mapped_column name string in models
Refactor: Moved from postgres.uuid to uuid in models
Fix: Schemas use Daf (Decimal as float to hack around pydantic v2 sending decimals as strings.)
2023-08-17 22:09:20 +05:30
tanshu e955ef4196 Version Bump v11.6.3 2023-08-07 17:45:35 +05:30
tanshu f1ef578eae Fix: Schema would show the full price for happy hour products 2023-08-07 17:43:09 +05:30
tanshu 05302c4291 Version Bump v11.6.2 2023-07-26 13:00:19 +05:30
tanshu 96e54e2a0a Fix: Repint was creating new bills. Rounding caused it to think that the amounts had changed.
Made the inventory amount expression round to even and the inventory schema to validate to round the inputs
2023-07-26 13:00:01 +05:30
tanshu 26310dfc42 Changed docker to use a hosts file with various var files and to use roles. 2023-07-23 08:46:45 +05:30
tanshu 16f29c4139 Version Bump v11.6.1 2023-07-22 14:57:57 +05:30
tanshu f8c1e0591b Fix: Discount not showing and other chores 2023-07-22 14:57:50 +05:30
tanshu 42102d7fef Version Bump v11.6.0 2023-07-22 09:22:14 +05:30
tanshu 09b762d154 Chore:
Moved to Angular 16
Moved to FastAPI 0.100.0
Moved to pydantic 2.0
2023-07-22 09:20:01 +05:30
tanshu 8b2d15b9e1 Fix: Guestbook would not let you seat anyone who has any old bill.
Fix: Only create a new temporal product when there is some change in the data.
2023-07-20 07:00:20 +05:30
tanshu 5534b3983d Moved back from devcontainer to normal as ubuntu now has python 3.11 2023-07-20 06:59:32 +05:30
tanshu e685941804 Version Bump v11.5.0 2023-04-09 15:43:00 +05:30
tanshu 8bc7d66123 Fix: Prevent creation of customer with blank name when adding a guest book item.
Fix: Also prevent creation of customer with blank phone number when adding a guest book.
Feature: Show the old bill of a customer in guest book
Fix: In reprint, allow changing of customer
Chore: Updated dependencies
2023-04-09 15:42:32 +05:30
tanshu 302ed4a18f Version Bump v11.4.2 2023-03-24 12:46:54 +05:30
tanshu 2fb2e01ca1 Feature: Allow edit of time in guest book.
Feature: Guest book row color is the same as running table colors
2023-03-24 12:46:42 +05:30
tanshu 94cc8ccd47 Version Bump v11.4.1 2023-03-24 10:17:16 +05:30
tanshu c13047e812 Fix: Check if moved/merged kots could lead to negative products of a situation where happy hour products were more than regular products.
Case:
  Kot 1:
    Beer Mug => 2 regular, 2 happy hour
  Kot 2:
    Beer Mug => 2 Regular
  Kot 3:
    Beer Mug => -2 Regular

Then move Kot 2 to new table would leave the voucher with 2 hh and 0 regular
Also, just move did not check if products were going negative.
2023-03-24 10:17:06 +05:30
tanshu 5c697dd43a Version Bump v11.4.0 2023-03-24 09:10:20 +05:30
tanshu 56ae7500cc Feature: Allow bills without items so that tables can be seated from the guest book.
Feature: Allow guest book entries to be associated with running vouchers
Feature: Allow removing customer from voucher
2023-03-24 09:09:13 +05:30
tanshu 1b4c26733d Chore: Refactor nil inventories and kots checking 2023-03-24 08:49:19 +05:30
tanshu 88bda14002 Fix: Typo 2023-03-24 08:48:33 +05:30
tanshu 7a8017087b Fix: Update pax if changed
Fix: Tax was associated with inventory, not just tax_id so that if along with adding an inventory, the bill is printed then it can be used to get regime information.
Chore: Refactored checking for nil inventory and kots.
2023-03-24 08:48:18 +05:30
tanshu 1297a4e7d2 Cancel VoucherType was added 2023-03-24 08:45:52 +05:30
tanshu 47afb7e29c Always ensure that a "KOT" Bill Number was generated even if the bill was straight printed. 2023-03-24 08:44:48 +05:30
tanshu 4db26722a3 Fix: Debugging was left in the report 2023-03-24 08:43:54 +05:30
tanshu b2db7e8f9c Version Bump v11.3.3 2023-03-19 23:08:22 +05:30
tanshu f6deea265c Fix: Error is checking length of bills 2023-03-19 23:08:14 +05:30
tanshu f1d145e76f Version Bump v11.3.2 2023-03-19 23:05:00 +05:30
tanshu 22f61427b0 Fix: Cashier report was only showing the last amount of aggregate items.
When it was not added to info, the amount also got reset to 0

Feature: Disabled total slip for single bills
2023-03-19 22:44:38 +05:30
tanshu f9fc6dba0d Version Bump v11.3.1 2023-03-19 14:43:45 +05:30
tanshu e56a753053 Fix: Users without ability of edit printed products where not able to set quantity to zero in the bill 2023-03-19 14:43:40 +05:30
tanshu 03918fee80 Version Bump v11.3.0 2023-03-19 09:56:58 +05:30
tanshu c14e64ce85 Feature: The menu engineering report now recalculates the percentages based on filter 2023-03-19 09:56:48 +05:30
tanshu 4e4cebf5d4 Chore: Moved the formatting to the main style sheet because it was used everywhere.
Also, moved to sass to get theme colors from the material design library
2023-03-18 23:29:10 +05:30
tanshu d39712a347 Feature: Made a Menu Engineering Report 2023-03-18 23:20:11 +05:30
tanshu 40a357edc8 Version Bump v11.2.0 2023-03-17 10:18:40 +05:30
tanshu 1a08066c2d Feature: Cancelled bills are now separate from void bills.
Void bills are automatically generated when printed bills are changed.
Manually cancelled bills are called cancelled bills.
2023-03-17 08:27:30 +05:30
tanshu a62018ad1f Version Bump v11.1.2 2023-03-14 13:27:40 +05:30
tanshu da46591313 Fix: Happy hour product button color was being overwritten by the face color 2023-03-14 13:27:05 +05:30
tanshu a1ccf973cd Version Bump v11.1.1 2023-03-14 00:15:50 +05:30
tanshu 65700d2760 Fix: Date was not updated when the bill was printed. 2023-03-14 00:15:35 +05:30
tanshu 77c7b167ef Version Bump v11.1.0 2023-03-13 23:52:56 +05:30
tanshu efa2af396d Chore: Moved from css to sass, god knows what to do now.
Chore: Prettier line length changed to 120 from 100
Fix: Hard coded the face as the primary color to make the buttons stand out
2023-03-13 23:52:44 +05:30
tanshu b021861ba3 Version Bump v11.0.2 2023-03-13 21:22:24 +05:30
tanshu 245937ecf2 Fix: The dialogs were not rendering properly.
The select table dialog was long and narrow.
The billtype was selection was also not showing properly.
2023-03-13 21:22:07 +05:30
tanshu 3af0b0ac96 Version Bump v11.0.1 2023-03-13 20:40:10 +05:30
tanshu 4fffdd85d6 Fix: Merge table did not delete the old bills and fucked up.
Fix: Bill settlement report was not in order
2023-03-13 20:38:55 +05:30
tanshu 7da9212cf7 Version Bump v11.0.0 2023-03-13 11:11:42 +05:30
tanshu 0f1660ef5a Fix: Regime was stripping the whitespaces from header 2023-03-06 22:02:21 +05:30
tanshu 7d0874093c Fix: New Product did not work due to missing quantity is the schema
Fix: New Product would not auto add modifiers
2023-03-06 21:42:09 +05:30
tanshu 516b22ed1c Refactored customer discount and choose discount. They were using the same schema unnecessarily which was leading to confusion. 2023-03-06 21:04:44 +05:30
tanshu c5eaeb1497 Fix: Receive payment did not show display reason 2023-03-06 10:35:17 +05:30
tanshu f408e84af3 Added traceback to save voucher to better understand why an error occurs. 2023-03-06 10:21:27 +05:30
tanshu e46fe7f90e Feature: Tax Regimes are added so that different bills with different series can be printed for Different regimes such as VAT and GST
Chore: Model relationships updated to make them simpler
Chore: Bill printing majorly refactored for it

Due to the sheer depth of the changes. There can be showstoppers. Please test it carefully
2023-03-05 23:50:41 +05:30
tanshu 802eded568 Due to update to sqlalchemy, fixed schema nullability 2023-02-26 19:51:29 +05:30
tanshu 5c7985e392 Chore:
Moved to sqlalchemy 2.0
Added type checking as much as possible
Updated angular to 15
Moved from Angular flex layout to tailwind css

Started developing on vscode with devcontainers
2023-02-20 18:49:17 +05:30
tanshu 792ccf923f Chore:
Upgraded to Angular 14
2022-07-06 09:04:10 +05:30
tanshu f637f01954 Version Bump v10.3.3 2022-06-21 12:40:35 +05:30
tanshu 236c746e1c Fix: Suddenly got errors saying that the redis.enqueue job was running in a different loop 2022-06-21 12:39:51 +05:30
tanshu a4db3b24f0 Version Bump v10.3.2 2022-06-21 08:54:51 +05:30
tanshu 9f4ec3697c Fix: Frank service changed to beacon from knox 2022-06-21 08:54:45 +05:30
tanshu 9fe84800a7 Chore: Dockerfile also updated 2022-06-21 08:53:37 +05:30
984 changed files with 33626 additions and 21817 deletions

No files matched your search

+31
View File
@@ -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"
]
}
}
}
+30
View File
@@ -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`.
+117
View File
@@ -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
+17
View File
@@ -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.
+19
View File
@@ -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.
+30
View File
@@ -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`).
+61
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
3.14
+14
View File
@@ -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"
]
}
+43
View File
@@ -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"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
{
// For more information, visit: https://angular.dev/ai/mcp
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
+79
View File
@@ -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": []
}
+48
View File
@@ -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
View File
@@ -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"]
+28
View File
@@ -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 \
-
+107 -29
View File
@@ -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.
+6 -5
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
{{ host }} {
reverse_proxy {{ host_directory }}:80
}
+14
View File
@@ -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
+20
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
---
collections:
- name: community.docker
+36
View File
@@ -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 }}"
+7
View File
@@ -0,0 +1,7 @@
---
caddy_container: caddy
caddy_caddyfile_path: /var/lib/caddy/conf/Caddyfile
# snippet_block: |
# {{ host }} {
# reverse_proxy {{ barker_container }}:80
# }
+5
View File
@@ -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
+27
View File
@@ -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
+10
View File
@@ -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
+8
View File
@@ -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"
+16
View File
@@ -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 }}"
+9
View File
@@ -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"
+8
View File
@@ -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"
+8
View File
@@ -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"
-3
View File
@@ -1,3 +0,0 @@
[flake8]
max-line-length = 120
exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache
-1
View File
@@ -1,4 +1,3 @@
*.pyc
*/__pycache__/
*.egg-info/
poetry.lock
+11 -17
View File
@@ -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
+22
View File
@@ -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"
}
}
+4 -3
View File
@@ -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
View File
@@ -1 +1 @@
__version__ = "10.3.1"
__version__ = "15.1.6"
-5
View File
@@ -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)
+5 -11
View File
@@ -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()
+14 -28
View File
@@ -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")
+19 -17
View File
@@ -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)
+56
View File
@@ -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
View File
@@ -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",
]
+2 -2
View File
@@ -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
+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from sqlalchemy.exc import IntegrityError
@dataclass(frozen=True)
class FriendlyDBError:
status: int
code: str
detail: str
constraint: str | None = None
# SQLSTATE → default mapping (Postgres)
_SQLSTATE_DEFAULTS: dict[str, tuple[int, str, str]] = {
"23505": (409, "UNIQUE_VIOLATION", "Already exists."),
"23503": (409, "FOREIGN_KEY_VIOLATION", "Referenced record not found."),
"23514": (400, "CHECK_VIOLATION", "Invalid value."),
"23502": (400, "NOT_NULL_VIOLATION", "Missing required value."),
}
# Constraint-name → friendly mapping for your schema
# Keep this as the "source of truth" for frontend-friendly messages.
_CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = {
# --- customers ---
"uq_customers_phone": FriendlyDBError(
409, "CUSTOMER_PHONE_EXISTS", "A customer with this phone number already exists."
),
"ck_customers_phone_not_blank": FriendlyDBError(400, "CUSTOMER_PHONE_BLANK", "Phone number cannot be blank."),
"ck_customers_name_not_blank": FriendlyDBError(400, "CUSTOMER_NAME_BLANK", "Customer name cannot be blank."),
# --- devices / tables / sections / printers / settings ---
"uq_devices_name": FriendlyDBError(409, "DEVICE_NAME_EXISTS", "A device with this name already exists."),
"uq_food_tables_name": FriendlyDBError(409, "FOOD_TABLE_NAME_EXISTS", "A table with this name already exists."),
"uq_sections_name": FriendlyDBError(409, "SECTION_NAME_EXISTS", "A section with this name already exists."),
"uq_printers_name": FriendlyDBError(409, "PRINTER_NAME_EXISTS", "A printer with this name already exists."),
"uq_settings_name": FriendlyDBError(409, "SETTING_NAME_EXISTS", "A setting with this name already exists."),
# --- regimes / taxes / sale_categories ---
"uq_regimes_name": FriendlyDBError(409, "REGIME_NAME_EXISTS", "A regime with this name already exists."),
"uq_regimes_prefix": FriendlyDBError(409, "REGIME_PREFIX_EXISTS", "A regime with this prefix already exists."),
"uq_taxes_name": FriendlyDBError(409, "TAX_NAME_EXISTS", "A tax with this name already exists."),
"uq_sale_categories_name": FriendlyDBError(
409, "SALE_CATEGORY_NAME_EXISTS", "A sale category with this name already exists."
),
# --- roles / permissions / settle_options ---
"uq_roles_name": FriendlyDBError(409, "ROLE_NAME_EXISTS", "A role with this name already exists."),
"uq_permissions_name": FriendlyDBError(
409, "PERMISSION_NAME_EXISTS", "A permission with this name already exists."
),
"uq_settle_options_name": FriendlyDBError(
409, "SETTLE_OPTION_NAME_EXISTS", "A settle option with this name already exists."
),
# --- kots / vouchers / bills / overview / settlements / reprints ---
"uq_kots_code": FriendlyDBError(409, "KOT_CODE_EXISTS", "This KOT code already exists."),
"uq_vouchers_kot_id": FriendlyDBError(409, "VOUCHER_FOR_KOT_EXISTS", "A voucher already exists for this KOT."),
"uq_bills_voucher_id": FriendlyDBError(409, "BILL_FOR_VOUCHER_EXISTS", "A bill already exists for this voucher."),
"uq_bills_regime_id": FriendlyDBError(409, "BILL_REGIME_CONFLICT", "Bill regime conflicts with an existing bill."),
"uq_overview_voucher_id": FriendlyDBError(
409, "OVERVIEW_FOR_VOUCHER_EXISTS", "An overview already exists for this voucher."
),
"uq_overview_food_table_id": FriendlyDBError(
409, "OVERVIEW_FOR_TABLE_EXISTS", "An overview already exists for this table."
),
"uq_overview_guest_book_id": FriendlyDBError(
409, "OVERVIEW_FOR_GUEST_EXISTS", "An overview already exists for this guest entry."
),
"uq_settlements_voucher_id": FriendlyDBError(
409, "SETTLEMENT_FOR_VOUCHER_EXISTS", "A settlement already exists for this voucher."
),
# --- login_history / user_roles / role_includes / role_permissions ---
"uq_login_history_user_id": FriendlyDBError(
409, "LOGIN_HISTORY_EXISTS", "Login history already exists for this user."
),
"uq_user_roles_user_id": FriendlyDBError(
409, "USER_ROLE_EXISTS", "A role assignment already exists for this user."
),
"uq_role_includes_role_id": FriendlyDBError(
409, "ROLE_INCLUDE_EXISTS", "This role-include relation already exists."
),
"uq_role_permissions_permission_id": FriendlyDBError(
409, "ROLE_PERMISSION_EXISTS", "This permission is already assigned."
),
# --- customer_discount ---
"uq_customer_discount_customer_id": FriendlyDBError(
409, "CUSTOMER_DISCOUNT_EXISTS", "Discount is already configured for this customer."
),
# --- inventory_modifiers / inventories ---
"uq_inventory_modifiers_inventory_id": FriendlyDBError(
409, "INVENTORY_MODIFIERS_EXISTS", "Modifiers already exist for this inventory item."
),
"uq_inventories_kot_id": FriendlyDBError(
409, "INVENTORY_FOR_KOT_EXISTS", "Inventory entry already exists for this KOT."
),
# --- menu_categories / modifier_categories / modifiers ---
"uq_menu_categories_name": FriendlyDBError(
409, "MENU_CATEGORY_NAME_EXISTS", "A menu category with this name already exists."
),
"uq_modifier_categories_name": FriendlyDBError(
409, "MODIFIER_CATEGORY_NAME_EXISTS", "A modifier category with this name already exists."
),
"uq_modifiers_name": FriendlyDBError(409, "MODIFIER_NAME_EXISTS", "A modifier with this name already exists."),
# --- exclude constraints (time-range overlaps / duplicates) ---
"uq_bundle_items_bundle_id_item_id": FriendlyDBError(
409, "BUNDLE_ITEM_EXISTS", "This item is already in the bundle."
),
"uq_product_versions_name": FriendlyDBError(
409,
"PRODUCT_VERSION_NAME_OVERLAP",
"Product name overlaps with an existing version in the selected date range.",
),
"uq_product_versions_product_id": FriendlyDBError(
409, "PRODUCT_VERSION_OVERLAP", "Product version overlaps with an existing version in the selected date range."
),
"uq_sku_versions_product_id_units": FriendlyDBError(
409, "SKU_PRODUCT_UNITS_OVERLAP", "This unit already exists for the product in the selected date range."
),
"uq_sku_versions_sku_id_units": FriendlyDBError(
409, "SKU_UNITS_OVERLAP", "This unit already exists for the SKU in the selected date range."
),
"uq_sku_versions_sku_id": FriendlyDBError(
409, "SKU_VERSION_OVERLAP", "SKU version overlaps with an existing version in the selected date range."
),
}
# --- UNIQUE INDEXES (not constraints) ---
_UNIQUE_INDEX_FRIENDLY: dict[str, FriendlyDBError] = {
"uq_users_name": FriendlyDBError(
409,
"USER_NAME_EXISTS",
"A user with this name already exists.",
),
}
# Optional: FK constraint names → friendlier messages (instead of generic “referenced record not found”)
# Add only the ones where you want specific UX.
_FK_FRIENDLY: dict[str, FriendlyDBError] = {
"fk_guest_book_customer_id_customers": FriendlyDBError(409, "CUSTOMER_NOT_FOUND", "Customer does not exist."),
"fk_vouchers_customer_id_customers": FriendlyDBError(409, "CUSTOMER_NOT_FOUND", "Customer does not exist."),
"fk_vouchers_food_table_id_food_tables": FriendlyDBError(409, "FOOD_TABLE_NOT_FOUND", "Food table does not exist."),
"fk_kots_food_table_id_food_tables": FriendlyDBError(409, "FOOD_TABLE_NOT_FOUND", "Food table does not exist."),
"fk_kots_user_id_users": FriendlyDBError(409, "USER_NOT_FOUND", "User does not exist."),
"fk_vouchers_user_id_users": FriendlyDBError(409, "USER_NOT_FOUND", "User does not exist."),
"fk_devices_section_id_sections": FriendlyDBError(409, "SECTION_NOT_FOUND", "Section does not exist."),
"fk_food_tables_section_id_sections": FriendlyDBError(409, "SECTION_NOT_FOUND", "Section does not exist."),
"fk_sku_versions_menu_category_id_menu_categories": FriendlyDBError(
409, "MENU_CATEGORY_NOT_FOUND", "Menu category does not exist."
),
"fk_stock_keeping_units_product_id_products": FriendlyDBError(409, "PRODUCT_NOT_FOUND", "Product does not exist."),
"fk_product_versions_product_id_products": FriendlyDBError(409, "PRODUCT_NOT_FOUND", "Product does not exist."),
"fk_products_sale_category_id_sale_categories": FriendlyDBError(
409, "SALE_CATEGORY_NOT_FOUND", "Sale category does not exist."
),
"fk_customer_discount_sale_category_id_sale_categories": FriendlyDBError(
409, "SALE_CATEGORY_NOT_FOUND", "Sale category does not exist."
),
"fk_sale_categories_tax_id_taxes": FriendlyDBError(409, "TAX_NOT_FOUND", "Tax does not exist."),
"fk_taxes_regime_id_regimes": FriendlyDBError(409, "REGIME_NOT_FOUND", "Regime does not exist."),
}
def _psycopg3_sqlstate(exc: IntegrityError) -> str | None:
orig = getattr(exc, "orig", None)
return getattr(orig, "sqlstate", None) if orig else None
def _psycopg3_constraint_name(exc: IntegrityError) -> str | None:
orig = getattr(exc, "orig", None)
diag = getattr(orig, "diag", None) if orig else None
return getattr(diag, "constraint_name", None) if diag else None
def constraint_to_friendly(exc: IntegrityError) -> FriendlyDBError:
"""
Convert a SQLAlchemy IntegrityError (psycopg3 / Postgres) to a frontend-friendly error.
Precedence:
A) Exact constraint-name mapping (_CONSTRAINT_FRIENDLY)
B) FK-specific mapping (_FK_FRIENDLY)
C) SQLSTATE generic mapping
D) Safe fallback
"""
sqlstate = _psycopg3_sqlstate(exc)
constraint = _psycopg3_constraint_name(exc)
if constraint:
if constraint in _CONSTRAINT_FRIENDLY:
e = _CONSTRAINT_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
if constraint in _UNIQUE_INDEX_FRIENDLY:
e = _UNIQUE_INDEX_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
if constraint in _FK_FRIENDLY:
e = _FK_FRIENDLY[constraint]
return FriendlyDBError(e.status, e.code, e.detail, constraint=constraint)
# common pattern: *_not_null constraints → 400 with field hint
if constraint.endswith("_not_null"):
# e.g. "customers_phone_not_null" → "phone"
field = re.sub(r"^.*?_", "", constraint).replace("_not_null", "")
return FriendlyDBError(
400,
"REQUIRED_FIELD_MISSING",
f"'{field}' is required.",
constraint=constraint,
)
if sqlstate is None:
status, code, detail = (409, "INTEGRITY_ERROR", "Conflict.")
else:
status, code, detail = _SQLSTATE_DEFAULTS.get(sqlstate, (409, "INTEGRITY_ERROR", "Conflict."))
return FriendlyDBError(status, code, detail, constraint=constraint)
-217
View File
@@ -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)
+19 -3
View File
@@ -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
View File
@@ -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()
+38
View File
@@ -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
+76
View File
@@ -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_
+33 -19
View File
@@ -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")
+35 -18
View File
@@ -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
+12 -11
View File
@@ -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
+34 -20
View File
@@ -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)
+31 -25
View File
@@ -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_
+63 -20
View File
@@ -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_
+10
View File
@@ -0,0 +1,10 @@
import enum
class GuestBookType(enum.StrEnum):
running = "running"
printed = "printed"
walk_in = "walk_in"
booking = "booking"
arrived = "arrived"
old = "old"
+101 -55
View File
@@ -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")
+27 -16
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
import enum
class InventoryType(enum.StrEnum):
regular = "regular"
bundle = "bundle"
bundle_item = "bundle_item"
+38 -27
View File
@@ -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
+33 -16
View File
@@ -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_
+26 -13
View File
@@ -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_
-17
View File
@@ -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)
+31 -23
View File
@@ -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",<