lambda-erp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. api/__init__.py +0 -0
  2. api/attachments.py +229 -0
  3. api/auth.py +511 -0
  4. api/bootstrap.py +498 -0
  5. api/chat.py +2764 -0
  6. api/demo_limits.py +400 -0
  7. api/deps.py +7 -0
  8. api/errors.py +56 -0
  9. api/main.py +182 -0
  10. api/pdf.py +151 -0
  11. api/providers.py +116 -0
  12. api/routers/__init__.py +0 -0
  13. api/routers/accounting.py +63 -0
  14. api/routers/admin.py +122 -0
  15. api/routers/analytics.py +1009 -0
  16. api/routers/bank_reconciliation.py +31 -0
  17. api/routers/documents.py +100 -0
  18. api/routers/masters.py +396 -0
  19. api/routers/reports.py +735 -0
  20. api/routers/setup.py +387 -0
  21. api/services.py +372 -0
  22. api/templates/document.html +197 -0
  23. lambda_erp/__init__.py +3 -0
  24. lambda_erp/accounting/__init__.py +0 -0
  25. lambda_erp/accounting/bank_transaction.py +76 -0
  26. lambda_erp/accounting/budget.py +117 -0
  27. lambda_erp/accounting/chart_of_accounts.py +183 -0
  28. lambda_erp/accounting/general_ledger.py +362 -0
  29. lambda_erp/accounting/journal_entry.py +235 -0
  30. lambda_erp/accounting/payment_entry.py +515 -0
  31. lambda_erp/accounting/pos_invoice.py +342 -0
  32. lambda_erp/accounting/purchase_invoice.py +504 -0
  33. lambda_erp/accounting/revaluation.py +172 -0
  34. lambda_erp/accounting/sales_invoice.py +523 -0
  35. lambda_erp/accounting/subscription.py +132 -0
  36. lambda_erp/buying/__init__.py +0 -0
  37. lambda_erp/buying/purchase_order.py +165 -0
  38. lambda_erp/controllers/__init__.py +0 -0
  39. lambda_erp/controllers/currency.py +52 -0
  40. lambda_erp/controllers/defaults.py +51 -0
  41. lambda_erp/controllers/pricing_rule.py +103 -0
  42. lambda_erp/controllers/taxes_and_totals.py +369 -0
  43. lambda_erp/database.py +1543 -0
  44. lambda_erp/exceptions.py +37 -0
  45. lambda_erp/hooks.py +37 -0
  46. lambda_erp/model.py +462 -0
  47. lambda_erp/selling/__init__.py +0 -0
  48. lambda_erp/selling/quotation.py +263 -0
  49. lambda_erp/selling/sales_order.py +214 -0
  50. lambda_erp/simulation.py +704 -0
  51. lambda_erp/stock/__init__.py +0 -0
  52. lambda_erp/stock/delivery_note.py +254 -0
  53. lambda_erp/stock/purchase_receipt.py +356 -0
  54. lambda_erp/stock/stock_entry.py +330 -0
  55. lambda_erp/stock/stock_ledger.py +337 -0
  56. lambda_erp/utils.py +167 -0
  57. lambda_erp-0.1.0.dist-info/METADATA +454 -0
  58. lambda_erp-0.1.0.dist-info/RECORD +60 -0
  59. lambda_erp-0.1.0.dist-info/WHEEL +4 -0
  60. lambda_erp-0.1.0.dist-info/licenses/LICENSE +21 -0
lambda_erp/database.py ADDED
@@ -0,0 +1,1543 @@
1
+ """
2
+ Lightweight SQLite database layer replacing the framework's database abstraction.
3
+
4
+ the framework uses framework.db.sql(), framework.db.get_value(), framework.db.get_all(), etc.
5
+ backed by MariaDB. This module provides the same interface on top of SQLite,
6
+ so the ported business logic can call db.get_value(...) the same way.
7
+
8
+ Schema is created from Python table definitions instead of DocType JSON files.
9
+ """
10
+
11
+ import datetime as _datetime
12
+ import sqlite3
13
+ import threading
14
+ from contextlib import contextmanager
15
+ from lambda_erp.utils import _dict
16
+
17
+
18
+ class _NullLock:
19
+ """No-op context manager used when SQLite-level locking is sufficient."""
20
+
21
+ def __enter__(self):
22
+ return self
23
+
24
+ def __exit__(self, *exc):
25
+ return False
26
+
27
+
28
+ class Database:
29
+ def __init__(self, db_path=":memory:"):
30
+ # Concurrency model:
31
+ # - File DBs: one sqlite3 connection per thread (thread-local), so the
32
+ # process can serve many WebSocket users without serializing every
33
+ # read behind a single Python lock. WAL allows concurrent readers
34
+ # plus one writer; busy_timeout makes the writer wait briefly
35
+ # instead of erroring with SQLITE_BUSY.
36
+ # - :memory: DBs (tests): each fresh sqlite connection to ":memory:"
37
+ # is a separate empty database, so we keep one shared connection
38
+ # guarded by a Python lock.
39
+ self.db_path = db_path
40
+ self._is_memory = (db_path == ":memory:")
41
+ self._local = threading.local()
42
+ if self._is_memory:
43
+ self._lock = threading.Lock()
44
+ self._shared_conn = self._open_conn()
45
+ self._shared_in_transaction = False
46
+ else:
47
+ self._lock = _NullLock()
48
+ self._shared_conn = None
49
+ # Open the init-thread connection now so _setup_schema can use it
50
+ # via the self.conn property.
51
+ self._open_conn()
52
+ self._setup_schema()
53
+
54
+ def _open_conn(self):
55
+ """Open a new sqlite connection with the same per-conn settings.
56
+
57
+ For file DBs the connection is stored on thread-local state so the
58
+ same thread reuses it; for :memory: it is returned and held as the
59
+ shared connection.
60
+ """
61
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
62
+ conn.row_factory = sqlite3.Row
63
+ conn.execute("PRAGMA journal_mode=WAL")
64
+ conn.execute("PRAGMA foreign_keys=ON")
65
+ # Wait up to 5s for the file lock instead of raising SQLITE_BUSY when
66
+ # another thread is mid-write. Generous for our workload (LLM calls
67
+ # dwarf any DB write).
68
+ conn.execute("PRAGMA busy_timeout=5000")
69
+ if not self._is_memory:
70
+ self._local.conn = conn
71
+ self._local.in_transaction = False
72
+ return conn
73
+
74
+ @property
75
+ def conn(self):
76
+ if self._is_memory:
77
+ return self._shared_conn
78
+ conn = getattr(self._local, "conn", None)
79
+ if conn is None:
80
+ conn = self._open_conn()
81
+ return conn
82
+
83
+ @property
84
+ def _in_transaction(self):
85
+ if self._is_memory:
86
+ return self._shared_in_transaction
87
+ return getattr(self._local, "in_transaction", False)
88
+
89
+ @_in_transaction.setter
90
+ def _in_transaction(self, value):
91
+ if self._is_memory:
92
+ self._shared_in_transaction = value
93
+ else:
94
+ self._local.in_transaction = value
95
+
96
+ def _setup_schema(self):
97
+ """Create all core ERP tables.
98
+
99
+ In the framework/the reference implementation, tables are auto-generated from DocType JSON.
100
+ Here we define them explicitly, matching the essential columns
101
+ that the business logic depends on.
102
+ """
103
+ stmts = [
104
+ # --- Master data ---
105
+ """CREATE TABLE IF NOT EXISTS "Company" (
106
+ name TEXT PRIMARY KEY,
107
+ company_name TEXT,
108
+ disabled INTEGER DEFAULT 0,
109
+ default_currency TEXT DEFAULT 'USD',
110
+ email TEXT,
111
+ phone TEXT,
112
+ address TEXT,
113
+ city TEXT,
114
+ zip_code TEXT,
115
+ country TEXT,
116
+ tax_id TEXT,
117
+ default_cost_center TEXT,
118
+ round_off_account TEXT,
119
+ round_off_cost_center TEXT,
120
+ default_receivable_account TEXT,
121
+ default_payable_account TEXT,
122
+ default_income_account TEXT,
123
+ default_expense_account TEXT,
124
+ stock_received_but_not_billed TEXT,
125
+ stock_in_hand_account TEXT,
126
+ stock_adjustment_account TEXT,
127
+ default_opening_balance_equity TEXT,
128
+ accumulated_depreciation_account TEXT,
129
+ depreciation_expense_account TEXT,
130
+ default_freight_in_account TEXT,
131
+ default_customs_account TEXT,
132
+ default_exchange_gain_loss_account TEXT,
133
+ default_unrealized_exchange_account TEXT
134
+ )""",
135
+
136
+ """CREATE TABLE IF NOT EXISTS "Account" (
137
+ name TEXT PRIMARY KEY,
138
+ account_name TEXT NOT NULL,
139
+ parent_account TEXT,
140
+ company TEXT,
141
+ root_type TEXT, -- Asset, Liability, Equity, Income, Expense
142
+ report_type TEXT, -- Balance Sheet, Profit and Loss
143
+ account_type TEXT, -- Receivable, Payable, Bank, Cash, Stock, etc.
144
+ account_currency TEXT DEFAULT 'USD',
145
+ is_group INTEGER DEFAULT 0,
146
+ disabled INTEGER DEFAULT 0,
147
+ lft INTEGER DEFAULT 0,
148
+ rgt INTEGER DEFAULT 0,
149
+ FOREIGN KEY (company) REFERENCES "Company"(name)
150
+ )""",
151
+
152
+ """CREATE TABLE IF NOT EXISTS "Cost Center" (
153
+ name TEXT PRIMARY KEY,
154
+ cost_center_name TEXT NOT NULL,
155
+ company TEXT,
156
+ parent_cost_center TEXT,
157
+ is_group INTEGER DEFAULT 0,
158
+ disabled INTEGER DEFAULT 0,
159
+ FOREIGN KEY (company) REFERENCES "Company"(name)
160
+ )""",
161
+
162
+ """CREATE TABLE IF NOT EXISTS "Customer" (
163
+ name TEXT PRIMARY KEY,
164
+ customer_name TEXT NOT NULL,
165
+ disabled INTEGER DEFAULT 0,
166
+ customer_group TEXT,
167
+ territory TEXT,
168
+ default_currency TEXT DEFAULT 'USD',
169
+ default_price_list TEXT,
170
+ credit_limit REAL DEFAULT 0,
171
+ email TEXT,
172
+ phone TEXT,
173
+ address TEXT,
174
+ city TEXT,
175
+ zip_code TEXT,
176
+ country TEXT,
177
+ tax_id TEXT
178
+ )""",
179
+
180
+ """CREATE TABLE IF NOT EXISTS "Supplier" (
181
+ name TEXT PRIMARY KEY,
182
+ supplier_name TEXT NOT NULL,
183
+ disabled INTEGER DEFAULT 0,
184
+ supplier_group TEXT,
185
+ default_currency TEXT DEFAULT 'USD',
186
+ email TEXT,
187
+ phone TEXT,
188
+ address TEXT,
189
+ city TEXT,
190
+ zip_code TEXT,
191
+ country TEXT,
192
+ tax_id TEXT
193
+ )""",
194
+
195
+ """CREATE TABLE IF NOT EXISTS "Item" (
196
+ name TEXT PRIMARY KEY,
197
+ item_name TEXT NOT NULL,
198
+ disabled INTEGER DEFAULT 0,
199
+ item_group TEXT,
200
+ stock_uom TEXT DEFAULT 'Nos',
201
+ is_stock_item INTEGER DEFAULT 1,
202
+ is_fixed_asset INTEGER DEFAULT 0,
203
+ valuation_method TEXT DEFAULT 'FIFO',
204
+ default_warehouse TEXT,
205
+ standard_rate REAL DEFAULT 0,
206
+ description TEXT
207
+ )""",
208
+
209
+ """CREATE TABLE IF NOT EXISTS "Warehouse" (
210
+ name TEXT PRIMARY KEY,
211
+ warehouse_name TEXT NOT NULL,
212
+ company TEXT,
213
+ parent_warehouse TEXT,
214
+ is_group INTEGER DEFAULT 0,
215
+ disabled INTEGER DEFAULT 0,
216
+ account TEXT,
217
+ address TEXT,
218
+ city TEXT,
219
+ zip_code TEXT,
220
+ country TEXT,
221
+ FOREIGN KEY (company) REFERENCES "Company"(name)
222
+ )""",
223
+
224
+ """CREATE TABLE IF NOT EXISTS "Fiscal Year" (
225
+ name TEXT PRIMARY KEY,
226
+ year_start_date TEXT,
227
+ year_end_date TEXT,
228
+ company TEXT
229
+ )""",
230
+
231
+ # --- Tax templates ---
232
+ """CREATE TABLE IF NOT EXISTS "Tax Template" (
233
+ name TEXT PRIMARY KEY,
234
+ title TEXT,
235
+ company TEXT,
236
+ tax_type TEXT -- 'Sales' or 'Purchase'
237
+ )""",
238
+
239
+ """CREATE TABLE IF NOT EXISTS "Tax Template Detail" (
240
+ name TEXT PRIMARY KEY,
241
+ parent TEXT,
242
+ charge_type TEXT DEFAULT 'On Net Total',
243
+ account_head TEXT,
244
+ rate REAL DEFAULT 0,
245
+ description TEXT,
246
+ idx INTEGER DEFAULT 0,
247
+ FOREIGN KEY (parent) REFERENCES "Tax Template"(name)
248
+ )""",
249
+
250
+ # --- GL Entry (the heart of accounting) ---
251
+ """CREATE TABLE IF NOT EXISTS "GL Entry" (
252
+ name TEXT PRIMARY KEY,
253
+ posting_date TEXT NOT NULL,
254
+ account TEXT NOT NULL,
255
+ party_type TEXT,
256
+ party TEXT,
257
+ cost_center TEXT,
258
+ debit REAL DEFAULT 0,
259
+ credit REAL DEFAULT 0,
260
+ debit_in_account_currency REAL DEFAULT 0,
261
+ credit_in_account_currency REAL DEFAULT 0,
262
+ account_currency TEXT DEFAULT 'USD',
263
+ voucher_type TEXT,
264
+ voucher_no TEXT,
265
+ against_voucher_type TEXT,
266
+ against_voucher TEXT,
267
+ remarks TEXT,
268
+ is_opening TEXT DEFAULT 'No',
269
+ is_cancelled INTEGER DEFAULT 0,
270
+ company TEXT,
271
+ fiscal_year TEXT,
272
+ creation TEXT,
273
+ modified TEXT
274
+ )""",
275
+
276
+ # --- Stock Ledger Entry (the heart of inventory) ---
277
+ """CREATE TABLE IF NOT EXISTS "Stock Ledger Entry" (
278
+ name TEXT PRIMARY KEY,
279
+ posting_date TEXT NOT NULL,
280
+ posting_time TEXT DEFAULT '00:00:00',
281
+ item_code TEXT NOT NULL,
282
+ warehouse TEXT NOT NULL,
283
+ actual_qty REAL DEFAULT 0,
284
+ qty_after_transaction REAL DEFAULT 0,
285
+ incoming_rate REAL DEFAULT 0,
286
+ outgoing_rate REAL DEFAULT 0,
287
+ valuation_rate REAL DEFAULT 0,
288
+ stock_value REAL DEFAULT 0,
289
+ stock_value_difference REAL DEFAULT 0,
290
+ voucher_type TEXT,
291
+ voucher_no TEXT,
292
+ voucher_detail_no TEXT,
293
+ batch_no TEXT,
294
+ serial_no TEXT,
295
+ company TEXT,
296
+ is_cancelled INTEGER DEFAULT 0,
297
+ creation TEXT,
298
+ modified TEXT
299
+ )""",
300
+
301
+ # --- Bin (current stock summary per item+warehouse) ---
302
+ """CREATE TABLE IF NOT EXISTS "Bin" (
303
+ name TEXT PRIMARY KEY,
304
+ item_code TEXT NOT NULL,
305
+ warehouse TEXT NOT NULL,
306
+ actual_qty REAL DEFAULT 0,
307
+ ordered_qty REAL DEFAULT 0,
308
+ reserved_qty REAL DEFAULT 0,
309
+ projected_qty REAL DEFAULT 0,
310
+ valuation_rate REAL DEFAULT 0,
311
+ stock_value REAL DEFAULT 0,
312
+ UNIQUE(item_code, warehouse)
313
+ )""",
314
+
315
+ # --- Quotation (offer / proposal) ---
316
+ """CREATE TABLE IF NOT EXISTS "Quotation" (
317
+ name TEXT PRIMARY KEY,
318
+ customer TEXT,
319
+ customer_name TEXT,
320
+ transaction_date TEXT,
321
+ valid_till TEXT,
322
+ company TEXT,
323
+ currency TEXT DEFAULT 'USD',
324
+ conversion_rate REAL DEFAULT 1.0,
325
+ total_qty REAL DEFAULT 0,
326
+ total REAL DEFAULT 0,
327
+ net_total REAL DEFAULT 0,
328
+ base_total REAL DEFAULT 0,
329
+ base_net_total REAL DEFAULT 0,
330
+ base_grand_total REAL DEFAULT 0,
331
+ grand_total REAL DEFAULT 0,
332
+ total_taxes_and_charges REAL DEFAULT 0,
333
+ discount_amount REAL DEFAULT 0,
334
+ apply_discount_on TEXT DEFAULT 'Grand Total',
335
+ remarks TEXT,
336
+ status TEXT DEFAULT 'Draft',
337
+ docstatus INTEGER DEFAULT 0,
338
+ creation TEXT,
339
+ modified TEXT
340
+ )""",
341
+
342
+ """CREATE TABLE IF NOT EXISTS "Quotation Item" (
343
+ name TEXT PRIMARY KEY,
344
+ parent TEXT,
345
+ idx INTEGER DEFAULT 0,
346
+ item_code TEXT,
347
+ item_name TEXT,
348
+ description TEXT,
349
+ qty REAL DEFAULT 0,
350
+ uom TEXT DEFAULT 'Nos',
351
+ rate REAL DEFAULT 0,
352
+ price_list_rate REAL DEFAULT 0,
353
+ discount_percentage REAL DEFAULT 0,
354
+ discount_amount REAL DEFAULT 0,
355
+ amount REAL DEFAULT 0,
356
+ net_rate REAL DEFAULT 0,
357
+ net_amount REAL DEFAULT 0,
358
+ base_rate REAL DEFAULT 0,
359
+ base_amount REAL DEFAULT 0,
360
+ base_net_rate REAL DEFAULT 0,
361
+ base_net_amount REAL DEFAULT 0,
362
+ warehouse TEXT,
363
+ FOREIGN KEY (parent) REFERENCES "Quotation"(name)
364
+ )""",
365
+
366
+ # --- Sales Order ---
367
+ """CREATE TABLE IF NOT EXISTS "Sales Order" (
368
+ name TEXT PRIMARY KEY,
369
+ customer TEXT,
370
+ customer_name TEXT,
371
+ transaction_date TEXT,
372
+ delivery_date TEXT,
373
+ company TEXT,
374
+ currency TEXT DEFAULT 'USD',
375
+ conversion_rate REAL DEFAULT 1.0,
376
+ total_qty REAL DEFAULT 0,
377
+ total REAL DEFAULT 0,
378
+ net_total REAL DEFAULT 0,
379
+ base_total REAL DEFAULT 0,
380
+ base_net_total REAL DEFAULT 0,
381
+ base_grand_total REAL DEFAULT 0,
382
+ grand_total REAL DEFAULT 0,
383
+ total_taxes_and_charges REAL DEFAULT 0,
384
+ discount_amount REAL DEFAULT 0,
385
+ apply_discount_on TEXT DEFAULT 'Grand Total',
386
+ per_delivered REAL DEFAULT 0,
387
+ per_billed REAL DEFAULT 0,
388
+ remarks TEXT,
389
+ status TEXT DEFAULT 'Draft',
390
+ docstatus INTEGER DEFAULT 0,
391
+ creation TEXT,
392
+ modified TEXT
393
+ )""",
394
+
395
+ """CREATE TABLE IF NOT EXISTS "Sales Order Item" (
396
+ name TEXT PRIMARY KEY,
397
+ parent TEXT,
398
+ idx INTEGER DEFAULT 0,
399
+ item_code TEXT,
400
+ item_name TEXT,
401
+ description TEXT,
402
+ qty REAL DEFAULT 0,
403
+ delivered_qty REAL DEFAULT 0,
404
+ billed_qty REAL DEFAULT 0,
405
+ uom TEXT DEFAULT 'Nos',
406
+ rate REAL DEFAULT 0,
407
+ price_list_rate REAL DEFAULT 0,
408
+ discount_percentage REAL DEFAULT 0,
409
+ discount_amount REAL DEFAULT 0,
410
+ amount REAL DEFAULT 0,
411
+ net_rate REAL DEFAULT 0,
412
+ net_amount REAL DEFAULT 0,
413
+ base_rate REAL DEFAULT 0,
414
+ base_amount REAL DEFAULT 0,
415
+ base_net_rate REAL DEFAULT 0,
416
+ base_net_amount REAL DEFAULT 0,
417
+ warehouse TEXT,
418
+ quotation_item TEXT,
419
+ FOREIGN KEY (parent) REFERENCES "Sales Order"(name)
420
+ )""",
421
+
422
+ # --- Purchase Order ---
423
+ """CREATE TABLE IF NOT EXISTS "Purchase Order" (
424
+ name TEXT PRIMARY KEY,
425
+ supplier TEXT,
426
+ supplier_name TEXT,
427
+ transaction_date TEXT,
428
+ schedule_date TEXT,
429
+ company TEXT,
430
+ currency TEXT DEFAULT 'USD',
431
+ conversion_rate REAL DEFAULT 1.0,
432
+ total_qty REAL DEFAULT 0,
433
+ total REAL DEFAULT 0,
434
+ net_total REAL DEFAULT 0,
435
+ base_total REAL DEFAULT 0,
436
+ base_net_total REAL DEFAULT 0,
437
+ base_grand_total REAL DEFAULT 0,
438
+ grand_total REAL DEFAULT 0,
439
+ total_taxes_and_charges REAL DEFAULT 0,
440
+ discount_amount REAL DEFAULT 0,
441
+ apply_discount_on TEXT DEFAULT 'Grand Total',
442
+ per_received REAL DEFAULT 0,
443
+ per_billed REAL DEFAULT 0,
444
+ remarks TEXT,
445
+ status TEXT DEFAULT 'Draft',
446
+ docstatus INTEGER DEFAULT 0,
447
+ creation TEXT,
448
+ modified TEXT
449
+ )""",
450
+
451
+ """CREATE TABLE IF NOT EXISTS "Purchase Order Item" (
452
+ name TEXT PRIMARY KEY,
453
+ parent TEXT,
454
+ idx INTEGER DEFAULT 0,
455
+ item_code TEXT,
456
+ item_name TEXT,
457
+ description TEXT,
458
+ qty REAL DEFAULT 0,
459
+ received_qty REAL DEFAULT 0,
460
+ billed_qty REAL DEFAULT 0,
461
+ uom TEXT DEFAULT 'Nos',
462
+ rate REAL DEFAULT 0,
463
+ price_list_rate REAL DEFAULT 0,
464
+ discount_percentage REAL DEFAULT 0,
465
+ discount_amount REAL DEFAULT 0,
466
+ amount REAL DEFAULT 0,
467
+ net_rate REAL DEFAULT 0,
468
+ net_amount REAL DEFAULT 0,
469
+ base_rate REAL DEFAULT 0,
470
+ base_amount REAL DEFAULT 0,
471
+ base_net_rate REAL DEFAULT 0,
472
+ base_net_amount REAL DEFAULT 0,
473
+ warehouse TEXT,
474
+ FOREIGN KEY (parent) REFERENCES "Purchase Order"(name)
475
+ )""",
476
+
477
+ # --- Sales Invoice ---
478
+ """CREATE TABLE IF NOT EXISTS "Sales Invoice" (
479
+ name TEXT PRIMARY KEY,
480
+ customer TEXT,
481
+ customer_name TEXT,
482
+ posting_date TEXT,
483
+ due_date TEXT,
484
+ company TEXT,
485
+ currency TEXT DEFAULT 'USD',
486
+ conversion_rate REAL DEFAULT 1.0,
487
+ debit_to TEXT, -- receivable account
488
+ total_qty REAL DEFAULT 0,
489
+ total REAL DEFAULT 0,
490
+ net_total REAL DEFAULT 0,
491
+ base_total REAL DEFAULT 0,
492
+ base_net_total REAL DEFAULT 0,
493
+ base_grand_total REAL DEFAULT 0,
494
+ grand_total REAL DEFAULT 0,
495
+ rounded_total REAL DEFAULT 0,
496
+ outstanding_amount REAL DEFAULT 0,
497
+ total_taxes_and_charges REAL DEFAULT 0,
498
+ discount_amount REAL DEFAULT 0,
499
+ apply_discount_on TEXT DEFAULT 'Grand Total',
500
+ is_return INTEGER DEFAULT 0,
501
+ return_against TEXT,
502
+ update_stock INTEGER DEFAULT 0,
503
+ is_pos INTEGER DEFAULT 0,
504
+ paid_amount REAL DEFAULT 0,
505
+ against_income_account TEXT,
506
+ sales_order TEXT,
507
+ per_billed REAL DEFAULT 0,
508
+ status TEXT DEFAULT 'Draft',
509
+ docstatus INTEGER DEFAULT 0,
510
+ remarks TEXT,
511
+ creation TEXT,
512
+ modified TEXT
513
+ )""",
514
+
515
+ """CREATE TABLE IF NOT EXISTS "Sales Invoice Item" (
516
+ name TEXT PRIMARY KEY,
517
+ parent TEXT,
518
+ idx INTEGER DEFAULT 0,
519
+ item_code TEXT,
520
+ item_name TEXT,
521
+ description TEXT,
522
+ qty REAL DEFAULT 0,
523
+ uom TEXT DEFAULT 'Nos',
524
+ rate REAL DEFAULT 0,
525
+ price_list_rate REAL DEFAULT 0,
526
+ discount_percentage REAL DEFAULT 0,
527
+ discount_amount REAL DEFAULT 0,
528
+ amount REAL DEFAULT 0,
529
+ net_rate REAL DEFAULT 0,
530
+ net_amount REAL DEFAULT 0,
531
+ base_rate REAL DEFAULT 0,
532
+ base_amount REAL DEFAULT 0,
533
+ base_net_rate REAL DEFAULT 0,
534
+ base_net_amount REAL DEFAULT 0,
535
+ income_account TEXT,
536
+ cost_center TEXT,
537
+ warehouse TEXT,
538
+ sales_order TEXT,
539
+ sales_order_item TEXT,
540
+ FOREIGN KEY (parent) REFERENCES "Sales Invoice"(name)
541
+ )""",
542
+
543
+ # --- Purchase Invoice ---
544
+ """CREATE TABLE IF NOT EXISTS "Purchase Invoice" (
545
+ name TEXT PRIMARY KEY,
546
+ supplier TEXT,
547
+ supplier_name TEXT,
548
+ posting_date TEXT,
549
+ due_date TEXT,
550
+ company TEXT,
551
+ currency TEXT DEFAULT 'USD',
552
+ conversion_rate REAL DEFAULT 1.0,
553
+ credit_to TEXT, -- payable account
554
+ total_qty REAL DEFAULT 0,
555
+ total REAL DEFAULT 0,
556
+ net_total REAL DEFAULT 0,
557
+ base_total REAL DEFAULT 0,
558
+ base_net_total REAL DEFAULT 0,
559
+ base_grand_total REAL DEFAULT 0,
560
+ grand_total REAL DEFAULT 0,
561
+ rounded_total REAL DEFAULT 0,
562
+ outstanding_amount REAL DEFAULT 0,
563
+ total_taxes_and_charges REAL DEFAULT 0,
564
+ discount_amount REAL DEFAULT 0,
565
+ apply_discount_on TEXT DEFAULT 'Grand Total',
566
+ is_return INTEGER DEFAULT 0,
567
+ return_against TEXT,
568
+ update_stock INTEGER DEFAULT 0,
569
+ against_expense_account TEXT,
570
+ purchase_order TEXT,
571
+ status TEXT DEFAULT 'Draft',
572
+ docstatus INTEGER DEFAULT 0,
573
+ remarks TEXT,
574
+ creation TEXT,
575
+ modified TEXT
576
+ )""",
577
+
578
+ """CREATE TABLE IF NOT EXISTS "Purchase Invoice Item" (
579
+ name TEXT PRIMARY KEY,
580
+ parent TEXT,
581
+ idx INTEGER DEFAULT 0,
582
+ item_code TEXT,
583
+ item_name TEXT,
584
+ description TEXT,
585
+ qty REAL DEFAULT 0,
586
+ uom TEXT DEFAULT 'Nos',
587
+ rate REAL DEFAULT 0,
588
+ price_list_rate REAL DEFAULT 0,
589
+ discount_percentage REAL DEFAULT 0,
590
+ discount_amount REAL DEFAULT 0,
591
+ amount REAL DEFAULT 0,
592
+ net_rate REAL DEFAULT 0,
593
+ net_amount REAL DEFAULT 0,
594
+ base_rate REAL DEFAULT 0,
595
+ base_amount REAL DEFAULT 0,
596
+ base_net_rate REAL DEFAULT 0,
597
+ base_net_amount REAL DEFAULT 0,
598
+ expense_account TEXT,
599
+ cost_center TEXT,
600
+ warehouse TEXT,
601
+ purchase_order TEXT,
602
+ purchase_order_item TEXT,
603
+ FOREIGN KEY (parent) REFERENCES "Purchase Invoice"(name)
604
+ )""",
605
+
606
+ # --- Payment Entry ---
607
+ """CREATE TABLE IF NOT EXISTS "Payment Entry" (
608
+ name TEXT PRIMARY KEY,
609
+ payment_type TEXT, -- Receive, Pay, Internal Transfer
610
+ posting_date TEXT,
611
+ company TEXT,
612
+ party_type TEXT, -- Customer, Supplier
613
+ party TEXT,
614
+ party_name TEXT,
615
+ paid_from TEXT, -- account
616
+ paid_to TEXT, -- account
617
+ paid_amount REAL DEFAULT 0,
618
+ received_amount REAL DEFAULT 0,
619
+ currency TEXT DEFAULT 'USD',
620
+ conversion_rate REAL DEFAULT 1.0,
621
+ reference_no TEXT,
622
+ reference_date TEXT,
623
+ cost_center TEXT,
624
+ status TEXT DEFAULT 'Draft',
625
+ docstatus INTEGER DEFAULT 0,
626
+ remarks TEXT,
627
+ creation TEXT,
628
+ modified TEXT
629
+ )""",
630
+
631
+ """CREATE TABLE IF NOT EXISTS "Payment Entry Reference" (
632
+ name TEXT PRIMARY KEY,
633
+ parent TEXT,
634
+ idx INTEGER DEFAULT 0,
635
+ reference_doctype TEXT,
636
+ reference_name TEXT,
637
+ total_amount REAL DEFAULT 0,
638
+ outstanding_amount REAL DEFAULT 0,
639
+ allocated_amount REAL DEFAULT 0,
640
+ FOREIGN KEY (parent) REFERENCES "Payment Entry"(name)
641
+ )""",
642
+
643
+ # --- Journal Entry ---
644
+ """CREATE TABLE IF NOT EXISTS "Journal Entry" (
645
+ name TEXT PRIMARY KEY,
646
+ posting_date TEXT,
647
+ company TEXT,
648
+ voucher_type TEXT DEFAULT 'Journal Entry',
649
+ total_debit REAL DEFAULT 0,
650
+ total_credit REAL DEFAULT 0,
651
+ remark TEXT,
652
+ status TEXT DEFAULT 'Draft',
653
+ docstatus INTEGER DEFAULT 0,
654
+ creation TEXT,
655
+ modified TEXT
656
+ )""",
657
+
658
+ """CREATE TABLE IF NOT EXISTS "Journal Entry Account" (
659
+ name TEXT PRIMARY KEY,
660
+ parent TEXT,
661
+ idx INTEGER DEFAULT 0,
662
+ account TEXT NOT NULL,
663
+ party_type TEXT,
664
+ party TEXT,
665
+ cost_center TEXT,
666
+ debit_in_account_currency REAL DEFAULT 0,
667
+ credit_in_account_currency REAL DEFAULT 0,
668
+ debit REAL DEFAULT 0,
669
+ credit REAL DEFAULT 0,
670
+ reference_type TEXT,
671
+ reference_name TEXT,
672
+ FOREIGN KEY (parent) REFERENCES "Journal Entry"(name)
673
+ )""",
674
+
675
+ # --- Stock Entry (material movement) ---
676
+ """CREATE TABLE IF NOT EXISTS "Stock Entry" (
677
+ name TEXT PRIMARY KEY,
678
+ stock_entry_type TEXT, -- Material Receipt, Material Issue, Material Transfer
679
+ posting_date TEXT,
680
+ posting_time TEXT DEFAULT '00:00:00',
681
+ company TEXT,
682
+ from_warehouse TEXT,
683
+ to_warehouse TEXT,
684
+ total_incoming_value REAL DEFAULT 0,
685
+ total_outgoing_value REAL DEFAULT 0,
686
+ value_difference REAL DEFAULT 0,
687
+ total_amount REAL DEFAULT 0,
688
+ status TEXT DEFAULT 'Draft',
689
+ docstatus INTEGER DEFAULT 0,
690
+ remarks TEXT,
691
+ creation TEXT,
692
+ modified TEXT
693
+ )""",
694
+
695
+ """CREATE TABLE IF NOT EXISTS "Stock Entry Detail" (
696
+ name TEXT PRIMARY KEY,
697
+ parent TEXT,
698
+ idx INTEGER DEFAULT 0,
699
+ item_code TEXT,
700
+ item_name TEXT,
701
+ qty REAL DEFAULT 0,
702
+ uom TEXT DEFAULT 'Nos',
703
+ s_warehouse TEXT, -- source
704
+ t_warehouse TEXT, -- target
705
+ basic_rate REAL DEFAULT 0,
706
+ basic_amount REAL DEFAULT 0,
707
+ valuation_rate REAL DEFAULT 0,
708
+ amount REAL DEFAULT 0,
709
+ FOREIGN KEY (parent) REFERENCES "Stock Entry"(name)
710
+ )""",
711
+
712
+ # --- Sales/Purchase Taxes and Charges (child table of transactions) ---
713
+ """CREATE TABLE IF NOT EXISTS "Sales Taxes and Charges" (
714
+ name TEXT PRIMARY KEY,
715
+ parent TEXT,
716
+ parenttype TEXT,
717
+ idx INTEGER DEFAULT 0,
718
+ charge_type TEXT DEFAULT 'On Net Total',
719
+ account_head TEXT,
720
+ description TEXT,
721
+ rate REAL DEFAULT 0,
722
+ tax_amount REAL DEFAULT 0,
723
+ total REAL DEFAULT 0,
724
+ base_tax_amount REAL DEFAULT 0,
725
+ base_total REAL DEFAULT 0,
726
+ included_in_print_rate INTEGER DEFAULT 0,
727
+ add_deduct_tax TEXT DEFAULT 'Add',
728
+ row_id INTEGER
729
+ )""",
730
+
731
+ # --- Delivery Note ---
732
+ """CREATE TABLE IF NOT EXISTS "Delivery Note" (
733
+ name TEXT PRIMARY KEY,
734
+ customer TEXT,
735
+ customer_name TEXT,
736
+ posting_date TEXT,
737
+ company TEXT,
738
+ currency TEXT DEFAULT 'USD',
739
+ conversion_rate REAL DEFAULT 1.0,
740
+ total_qty REAL DEFAULT 0,
741
+ total REAL DEFAULT 0,
742
+ net_total REAL DEFAULT 0,
743
+ base_net_total REAL DEFAULT 0,
744
+ base_grand_total REAL DEFAULT 0,
745
+ grand_total REAL DEFAULT 0,
746
+ total_taxes_and_charges REAL DEFAULT 0,
747
+ per_billed REAL DEFAULT 0,
748
+ is_return INTEGER DEFAULT 0,
749
+ return_against TEXT,
750
+ status TEXT DEFAULT 'Draft',
751
+ docstatus INTEGER DEFAULT 0,
752
+ remarks TEXT,
753
+ creation TEXT,
754
+ modified TEXT
755
+ )""",
756
+
757
+ """CREATE TABLE IF NOT EXISTS "Delivery Note Item" (
758
+ name TEXT PRIMARY KEY,
759
+ parent TEXT,
760
+ idx INTEGER DEFAULT 0,
761
+ item_code TEXT,
762
+ item_name TEXT,
763
+ description TEXT,
764
+ qty REAL DEFAULT 0,
765
+ uom TEXT DEFAULT 'Nos',
766
+ rate REAL DEFAULT 0,
767
+ price_list_rate REAL DEFAULT 0,
768
+ discount_percentage REAL DEFAULT 0,
769
+ discount_amount REAL DEFAULT 0,
770
+ amount REAL DEFAULT 0,
771
+ net_rate REAL DEFAULT 0,
772
+ net_amount REAL DEFAULT 0,
773
+ base_rate REAL DEFAULT 0,
774
+ base_amount REAL DEFAULT 0,
775
+ base_net_rate REAL DEFAULT 0,
776
+ base_net_amount REAL DEFAULT 0,
777
+ warehouse TEXT,
778
+ against_sales_order TEXT,
779
+ so_detail TEXT,
780
+ FOREIGN KEY (parent) REFERENCES "Delivery Note"(name)
781
+ )""",
782
+
783
+ # --- Purchase Receipt ---
784
+ """CREATE TABLE IF NOT EXISTS "Purchase Receipt" (
785
+ name TEXT PRIMARY KEY,
786
+ supplier TEXT,
787
+ supplier_name TEXT,
788
+ posting_date TEXT,
789
+ company TEXT,
790
+ currency TEXT DEFAULT 'USD',
791
+ conversion_rate REAL DEFAULT 1.0,
792
+ total_qty REAL DEFAULT 0,
793
+ total REAL DEFAULT 0,
794
+ net_total REAL DEFAULT 0,
795
+ base_net_total REAL DEFAULT 0,
796
+ base_grand_total REAL DEFAULT 0,
797
+ grand_total REAL DEFAULT 0,
798
+ total_taxes_and_charges REAL DEFAULT 0,
799
+ per_billed REAL DEFAULT 0,
800
+ is_return INTEGER DEFAULT 0,
801
+ return_against TEXT,
802
+ status TEXT DEFAULT 'Draft',
803
+ docstatus INTEGER DEFAULT 0,
804
+ remarks TEXT,
805
+ creation TEXT,
806
+ modified TEXT
807
+ )""",
808
+
809
+ """CREATE TABLE IF NOT EXISTS "Purchase Receipt Item" (
810
+ name TEXT PRIMARY KEY,
811
+ parent TEXT,
812
+ idx INTEGER DEFAULT 0,
813
+ item_code TEXT,
814
+ item_name TEXT,
815
+ description TEXT,
816
+ qty REAL DEFAULT 0,
817
+ uom TEXT DEFAULT 'Nos',
818
+ rate REAL DEFAULT 0,
819
+ price_list_rate REAL DEFAULT 0,
820
+ discount_percentage REAL DEFAULT 0,
821
+ discount_amount REAL DEFAULT 0,
822
+ amount REAL DEFAULT 0,
823
+ net_rate REAL DEFAULT 0,
824
+ net_amount REAL DEFAULT 0,
825
+ base_rate REAL DEFAULT 0,
826
+ base_amount REAL DEFAULT 0,
827
+ base_net_rate REAL DEFAULT 0,
828
+ base_net_amount REAL DEFAULT 0,
829
+ warehouse TEXT,
830
+ against_purchase_order TEXT,
831
+ po_detail TEXT,
832
+ FOREIGN KEY (parent) REFERENCES "Purchase Receipt"(name)
833
+ )""",
834
+
835
+ # --- POS Invoice ---
836
+ """CREATE TABLE IF NOT EXISTS "POS Invoice" (
837
+ name TEXT PRIMARY KEY,
838
+ customer TEXT,
839
+ customer_name TEXT,
840
+ posting_date TEXT,
841
+ company TEXT,
842
+ currency TEXT DEFAULT 'USD',
843
+ conversion_rate REAL DEFAULT 1.0,
844
+ debit_to TEXT,
845
+ total_qty REAL DEFAULT 0,
846
+ total REAL DEFAULT 0,
847
+ net_total REAL DEFAULT 0,
848
+ base_net_total REAL DEFAULT 0,
849
+ base_grand_total REAL DEFAULT 0,
850
+ grand_total REAL DEFAULT 0,
851
+ rounded_total REAL DEFAULT 0,
852
+ total_taxes_and_charges REAL DEFAULT 0,
853
+ paid_amount REAL DEFAULT 0,
854
+ outstanding_amount REAL DEFAULT 0,
855
+ change_amount REAL DEFAULT 0,
856
+ update_stock INTEGER DEFAULT 1,
857
+ is_return INTEGER DEFAULT 0,
858
+ return_against TEXT,
859
+ status TEXT DEFAULT 'Draft',
860
+ docstatus INTEGER DEFAULT 0,
861
+ remarks TEXT,
862
+ creation TEXT,
863
+ modified TEXT
864
+ )""",
865
+
866
+ """CREATE TABLE IF NOT EXISTS "POS Invoice Item" (
867
+ name TEXT PRIMARY KEY,
868
+ parent TEXT,
869
+ idx INTEGER DEFAULT 0,
870
+ item_code TEXT,
871
+ item_name TEXT,
872
+ description TEXT,
873
+ qty REAL DEFAULT 0,
874
+ uom TEXT DEFAULT 'Nos',
875
+ rate REAL DEFAULT 0,
876
+ price_list_rate REAL DEFAULT 0,
877
+ discount_percentage REAL DEFAULT 0,
878
+ discount_amount REAL DEFAULT 0,
879
+ amount REAL DEFAULT 0,
880
+ net_rate REAL DEFAULT 0,
881
+ net_amount REAL DEFAULT 0,
882
+ base_rate REAL DEFAULT 0,
883
+ base_amount REAL DEFAULT 0,
884
+ base_net_rate REAL DEFAULT 0,
885
+ base_net_amount REAL DEFAULT 0,
886
+ income_account TEXT,
887
+ cost_center TEXT,
888
+ warehouse TEXT,
889
+ FOREIGN KEY (parent) REFERENCES "POS Invoice"(name)
890
+ )""",
891
+
892
+ """CREATE TABLE IF NOT EXISTS "POS Invoice Payment" (
893
+ name TEXT PRIMARY KEY,
894
+ parent TEXT,
895
+ idx INTEGER DEFAULT 0,
896
+ mode_of_payment TEXT,
897
+ account TEXT,
898
+ amount REAL DEFAULT 0,
899
+ FOREIGN KEY (parent) REFERENCES "POS Invoice"(name)
900
+ )""",
901
+
902
+ # --- Pricing Rule ---
903
+ """CREATE TABLE IF NOT EXISTS "Pricing Rule" (
904
+ name TEXT PRIMARY KEY,
905
+ title TEXT,
906
+ item_code TEXT,
907
+ selling INTEGER DEFAULT 0,
908
+ buying INTEGER DEFAULT 0,
909
+ rate_or_discount TEXT DEFAULT 'Discount Percentage',
910
+ rate REAL DEFAULT 0,
911
+ discount_percentage REAL DEFAULT 0,
912
+ discount_amount REAL DEFAULT 0,
913
+ min_qty REAL DEFAULT 0,
914
+ valid_from TEXT,
915
+ valid_upto TEXT,
916
+ priority INTEGER DEFAULT 0,
917
+ company TEXT,
918
+ enabled INTEGER DEFAULT 1,
919
+ status TEXT DEFAULT 'Active',
920
+ docstatus INTEGER DEFAULT 0,
921
+ creation TEXT,
922
+ modified TEXT
923
+ )""",
924
+
925
+ # --- Budget ---
926
+ """CREATE TABLE IF NOT EXISTS "Budget" (
927
+ name TEXT PRIMARY KEY,
928
+ budget_against TEXT DEFAULT 'Cost Center',
929
+ cost_center TEXT,
930
+ account TEXT,
931
+ fiscal_year TEXT,
932
+ company TEXT,
933
+ budget_amount REAL DEFAULT 0,
934
+ action_if_exceeded TEXT DEFAULT 'Warn',
935
+ status TEXT DEFAULT 'Active',
936
+ docstatus INTEGER DEFAULT 0,
937
+ creation TEXT,
938
+ modified TEXT
939
+ )""",
940
+
941
+ """CREATE TABLE IF NOT EXISTS "Monthly Distribution" (
942
+ name TEXT PRIMARY KEY,
943
+ parent TEXT,
944
+ idx INTEGER DEFAULT 0,
945
+ month TEXT,
946
+ percentage REAL DEFAULT 0,
947
+ FOREIGN KEY (parent) REFERENCES "Budget"(name)
948
+ )""",
949
+
950
+ # --- Subscription ---
951
+ """CREATE TABLE IF NOT EXISTS "Subscription" (
952
+ name TEXT PRIMARY KEY,
953
+ party_type TEXT,
954
+ party TEXT,
955
+ company TEXT,
956
+ start_date TEXT,
957
+ end_date TEXT,
958
+ billing_interval TEXT DEFAULT 'Monthly',
959
+ current_invoice_start TEXT,
960
+ current_invoice_end TEXT,
961
+ status TEXT DEFAULT 'Active',
962
+ docstatus INTEGER DEFAULT 0,
963
+ creation TEXT,
964
+ modified TEXT
965
+ )""",
966
+
967
+ """CREATE TABLE IF NOT EXISTS "Subscription Plan" (
968
+ name TEXT PRIMARY KEY,
969
+ parent TEXT,
970
+ idx INTEGER DEFAULT 0,
971
+ item_code TEXT,
972
+ item_name TEXT,
973
+ qty REAL DEFAULT 1,
974
+ rate REAL DEFAULT 0,
975
+ FOREIGN KEY (parent) REFERENCES "Subscription"(name)
976
+ )""",
977
+
978
+ # --- Bank Transaction ---
979
+ """CREATE TABLE IF NOT EXISTS "Bank Transaction" (
980
+ name TEXT PRIMARY KEY,
981
+ bank_account TEXT,
982
+ posting_date TEXT,
983
+ deposit REAL DEFAULT 0,
984
+ withdrawal REAL DEFAULT 0,
985
+ description TEXT,
986
+ reference_number TEXT,
987
+ allocated_amount REAL DEFAULT 0,
988
+ unallocated_amount REAL DEFAULT 0,
989
+ reference_doctype TEXT,
990
+ reference_name TEXT,
991
+ status TEXT DEFAULT 'Unreconciled',
992
+ docstatus INTEGER DEFAULT 0,
993
+ creation TEXT,
994
+ modified TEXT
995
+ )""",
996
+
997
+ # --- Chat Sessions ---
998
+ """CREATE TABLE IF NOT EXISTS "Chat Session" (
999
+ id TEXT PRIMARY KEY,
1000
+ title TEXT DEFAULT 'New Chat',
1001
+ user_id TEXT,
1002
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1003
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1004
+ )""",
1005
+
1006
+ # --- Chat Messages (conversation persistence) ---
1007
+ """CREATE TABLE IF NOT EXISTS "Chat Message" (
1008
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1009
+ session_id TEXT,
1010
+ role TEXT NOT NULL,
1011
+ message_type TEXT DEFAULT 'chat',
1012
+ content TEXT,
1013
+ metadata_json TEXT,
1014
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1015
+ FOREIGN KEY (session_id) REFERENCES "Chat Session"(id)
1016
+ )""",
1017
+
1018
+ # --- Chat Attachments (PDFs + images uploaded via chat) ---
1019
+ """CREATE TABLE IF NOT EXISTS "Chat Attachment" (
1020
+ id TEXT PRIMARY KEY,
1021
+ session_id TEXT NOT NULL,
1022
+ user_id TEXT,
1023
+ filename TEXT NOT NULL,
1024
+ mime_type TEXT NOT NULL,
1025
+ size_bytes INTEGER NOT NULL,
1026
+ file_path TEXT NOT NULL,
1027
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1028
+ FOREIGN KEY (session_id) REFERENCES "Chat Session"(id)
1029
+ )""",
1030
+
1031
+ # --- Report Drafts (chat-authored analytics reports) ---
1032
+ """CREATE TABLE IF NOT EXISTS "Report Draft" (
1033
+ id TEXT PRIMARY KEY,
1034
+ title TEXT NOT NULL,
1035
+ description TEXT,
1036
+ definition_json TEXT NOT NULL,
1037
+ created_by TEXT,
1038
+ source_chat_session_id TEXT,
1039
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
1040
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
1041
+ )""",
1042
+
1043
+ # --- Authentication ---
1044
+ """CREATE TABLE IF NOT EXISTS "User" (
1045
+ name TEXT PRIMARY KEY,
1046
+ email TEXT NOT NULL UNIQUE,
1047
+ full_name TEXT NOT NULL,
1048
+ hashed_password TEXT NOT NULL,
1049
+ role TEXT NOT NULL DEFAULT 'viewer',
1050
+ enabled INTEGER DEFAULT 1,
1051
+ creation TEXT,
1052
+ modified TEXT
1053
+ )""",
1054
+
1055
+ """CREATE TABLE IF NOT EXISTS "Invite" (
1056
+ token TEXT PRIMARY KEY,
1057
+ email TEXT NOT NULL,
1058
+ role TEXT NOT NULL DEFAULT 'viewer',
1059
+ created_by TEXT,
1060
+ used INTEGER DEFAULT 0,
1061
+ creation TEXT,
1062
+ FOREIGN KEY (created_by) REFERENCES "User"(name)
1063
+ )""",
1064
+
1065
+ """CREATE TABLE IF NOT EXISTS "Settings" (
1066
+ key TEXT PRIMARY KEY,
1067
+ value TEXT
1068
+ )""",
1069
+
1070
+ # Exchange rates for multi-currency. A lookup carries forward the
1071
+ # most recent rate on/before a transaction's date; the rate is then
1072
+ # snapshotted onto the document, so editing this table never changes
1073
+ # already-posted books. exchange_rate = units of to_currency per 1
1074
+ # unit of from_currency (e.g. EUR->USD 1.10 means 1 EUR = 1.10 USD).
1075
+ """CREATE TABLE IF NOT EXISTS "Currency Exchange" (
1076
+ name TEXT PRIMARY KEY,
1077
+ date TEXT,
1078
+ from_currency TEXT,
1079
+ to_currency TEXT,
1080
+ exchange_rate REAL,
1081
+ creation TEXT,
1082
+ modified TEXT
1083
+ )""",
1084
+ ]
1085
+
1086
+ for stmt in stmts:
1087
+ self.conn.execute(stmt)
1088
+ self.conn.commit()
1089
+ self._migrate()
1090
+
1091
+ # -----------------------------------------------------------------
1092
+ # Migrations
1093
+ #
1094
+ # Each entry is (version, name, callable). The runner applies each
1095
+ # migration exactly once, records it in _SchemaMigrations, and commits.
1096
+ # Versions are monotonically increasing integers — do not renumber or
1097
+ # remove existing migrations. To add a new one, append the next version.
1098
+ # Migrations must be idempotent: they run against both fresh databases
1099
+ # (after CREATE TABLE) and existing databases that may already have the
1100
+ # new columns from earlier ad-hoc ALTERs, so guard with PRAGMA checks.
1101
+ # -----------------------------------------------------------------
1102
+
1103
+ MIGRATIONS = None # populated just below __init_subclass__ — see end of class
1104
+
1105
+ def _add_column_if_missing(self, table: str, column: str, definition: str) -> None:
1106
+ cols = {row[1] for row in self.conn.execute(f'PRAGMA table_info("{table}")').fetchall()}
1107
+ if column not in cols:
1108
+ self.conn.execute(f'ALTER TABLE "{table}" ADD COLUMN {column} {definition}')
1109
+
1110
+ def _migrate(self):
1111
+ """Run each pending migration in order, tracking applied versions."""
1112
+ self.conn.execute(
1113
+ """CREATE TABLE IF NOT EXISTS "_SchemaMigrations" (
1114
+ version INTEGER PRIMARY KEY,
1115
+ name TEXT NOT NULL,
1116
+ applied_at TEXT NOT NULL
1117
+ )"""
1118
+ )
1119
+ self.conn.commit()
1120
+
1121
+ applied = {
1122
+ row[0]
1123
+ for row in self.conn.execute('SELECT version FROM "_SchemaMigrations"').fetchall()
1124
+ }
1125
+
1126
+ for version, name, fn in self.MIGRATIONS:
1127
+ if version in applied:
1128
+ continue
1129
+ try:
1130
+ fn(self)
1131
+ self.conn.execute(
1132
+ 'INSERT INTO "_SchemaMigrations" (version, name, applied_at) VALUES (?, ?, ?)',
1133
+ [version, name, _datetime.datetime.utcnow().isoformat(timespec="seconds")],
1134
+ )
1135
+ self.conn.commit()
1136
+ except Exception:
1137
+ # Keep the DB usable even if one migration fails (e.g. the
1138
+ # column already exists from a prior ad-hoc ALTER on a
1139
+ # long-lived database). The CREATE TABLE at startup already
1140
+ # covers the happy path; migrations are only needed for drift.
1141
+ self.conn.rollback()
1142
+
1143
+ # --- Core query interface (mirrors framework.db) ---
1144
+
1145
+ def sql(self, query, values=None, as_dict=True):
1146
+ """Execute raw SQL. Mirrors framework.db.sql().
1147
+
1148
+ For file DBs the lock is a no-op: each thread has its own connection,
1149
+ and SQLite's own file-level locking + WAL is enough. For :memory:
1150
+ the Python lock prevents concurrent execute() calls on the single
1151
+ shared connection (which otherwise raises "bad parameter or other
1152
+ API misuse").
1153
+ """
1154
+ with self._lock:
1155
+ cursor = self.conn.execute(query, values or [])
1156
+ rows = cursor.fetchall()
1157
+ if as_dict:
1158
+ return [_dict(dict(row)) for row in rows]
1159
+ return [tuple(row) for row in rows]
1160
+
1161
+ def get_value(self, doctype, name, fieldname=None, filters=None):
1162
+ """Get a single field value. Mirrors framework.db.get_value().
1163
+
1164
+ Usage:
1165
+ db.get_value("Customer", "CUST-001", "customer_name")
1166
+ db.get_value("Customer", {"customer_group": "Retail"}, "name")
1167
+ """
1168
+ if fieldname is None:
1169
+ fieldname = "name"
1170
+
1171
+ fields = fieldname if isinstance(fieldname, (list, tuple)) else [fieldname]
1172
+ field_str = ", ".join(f'"{f}"' for f in fields)
1173
+
1174
+ if isinstance(name, dict) or filters:
1175
+ filt = name if isinstance(name, dict) else (filters or {})
1176
+ where_parts = []
1177
+ params = []
1178
+ for k, v in filt.items():
1179
+ where_parts.append(f'"{k}" = ?')
1180
+ params.append(v)
1181
+ where = " AND ".join(where_parts) if where_parts else "1=1"
1182
+ query = f'SELECT {field_str} FROM "{doctype}" WHERE {where} LIMIT 1'
1183
+ rows = self.sql(query, params)
1184
+ else:
1185
+ query = f'SELECT {field_str} FROM "{doctype}" WHERE name = ? LIMIT 1'
1186
+ rows = self.sql(query, [name])
1187
+
1188
+ if not rows:
1189
+ return None
1190
+
1191
+ if isinstance(fieldname, (list, tuple)):
1192
+ return rows[0]
1193
+ return rows[0].get(fieldname)
1194
+
1195
+ def get_all(self, doctype, filters=None, fields=None, order_by=None, limit=None):
1196
+ """Get all matching records. Mirrors framework.db.get_all()."""
1197
+ if fields is None:
1198
+ fields = ["name"]
1199
+ if isinstance(fields, str):
1200
+ fields = [fields]
1201
+
1202
+ if fields == ["*"]:
1203
+ field_str = "*"
1204
+ else:
1205
+ field_str = ", ".join(f'"{f}"' for f in fields)
1206
+ query = f'SELECT {field_str} FROM "{doctype}"'
1207
+
1208
+ params = []
1209
+ if filters:
1210
+ where_parts = []
1211
+ for k, v in filters.items():
1212
+ if isinstance(v, (list, tuple)) and len(v) == 2:
1213
+ op, val = v
1214
+ where_parts.append(f'"{k}" {op} ?')
1215
+ params.append(val)
1216
+ else:
1217
+ where_parts.append(f'"{k}" = ?')
1218
+ params.append(v)
1219
+ query += " WHERE " + " AND ".join(where_parts)
1220
+
1221
+ if order_by:
1222
+ query += f" ORDER BY {order_by}"
1223
+ if limit:
1224
+ query += f" LIMIT {limit}"
1225
+
1226
+ return self.sql(query, params)
1227
+
1228
+ def set_value(self, doctype, name, fieldname, value=None):
1229
+ """Set a single field value. Mirrors framework.db.set_value()."""
1230
+ if isinstance(fieldname, dict):
1231
+ sets = ", ".join(f'"{k}" = ?' for k in fieldname)
1232
+ params = list(fieldname.values()) + [name]
1233
+ else:
1234
+ sets = f'"{fieldname}" = ?'
1235
+ params = [value, name]
1236
+
1237
+ with self._lock:
1238
+ self.conn.execute(f'UPDATE "{doctype}" SET {sets} WHERE name = ?', params)
1239
+ if not self._in_transaction:
1240
+ self.conn.commit()
1241
+
1242
+ def exists(self, doctype, name=None, filters=None):
1243
+ """Check if a record exists."""
1244
+ if name and not filters:
1245
+ rows = self.sql(f'SELECT name FROM "{doctype}" WHERE name = ?', [name])
1246
+ elif filters:
1247
+ where_parts = []
1248
+ params = []
1249
+ for k, v in filters.items():
1250
+ where_parts.append(f'"{k}" = ?')
1251
+ params.append(v)
1252
+ where = " AND ".join(where_parts)
1253
+ rows = self.sql(f'SELECT name FROM "{doctype}" WHERE {where} LIMIT 1', params)
1254
+ else:
1255
+ return False
1256
+ return bool(rows)
1257
+
1258
+ def _get_table_columns(self, doctype):
1259
+ """Get column names for a table."""
1260
+ cursor = self.conn.execute(f'PRAGMA table_info("{doctype}")')
1261
+ return {row[1] for row in cursor.fetchall()}
1262
+
1263
+ def insert(self, doctype, doc):
1264
+ """Insert a record from a dict. Ignores fields not in the table schema."""
1265
+ valid_columns = self._get_table_columns(doctype)
1266
+ fields = [f for f in doc.keys() if f in valid_columns]
1267
+ if not fields:
1268
+ return
1269
+ placeholders = ", ".join(["?"] * len(fields))
1270
+ field_str = ", ".join(f'"{f}"' for f in fields)
1271
+ values = [doc[f] for f in fields]
1272
+ with self._lock:
1273
+ self.conn.execute(
1274
+ f'INSERT INTO "{doctype}" ({field_str}) VALUES ({placeholders})', values
1275
+ )
1276
+ if not self._in_transaction:
1277
+ self.conn.commit()
1278
+
1279
+ def insert_many(self, doctype, docs):
1280
+ """Insert multiple records."""
1281
+ if not docs:
1282
+ return
1283
+ fields = list(docs[0].keys())
1284
+ placeholders = ", ".join(["?"] * len(fields))
1285
+ field_str = ", ".join(f'"{f}"' for f in fields)
1286
+ values = [[doc.get(f) for f in fields] for doc in docs]
1287
+ self.conn.executemany(
1288
+ f'INSERT INTO "{doctype}" ({field_str}) VALUES ({placeholders})', values
1289
+ )
1290
+ if not self._in_transaction:
1291
+ self.conn.commit()
1292
+
1293
+ def delete(self, doctype, name=None, filters=None):
1294
+ """Delete a record."""
1295
+ with self._lock:
1296
+ if name:
1297
+ self.conn.execute(f'DELETE FROM "{doctype}" WHERE name = ?', [name])
1298
+ elif filters:
1299
+ where_parts = []
1300
+ params = []
1301
+ for k, v in filters.items():
1302
+ where_parts.append(f'"{k}" = ?')
1303
+ params.append(v)
1304
+ where = " AND ".join(where_parts)
1305
+ self.conn.execute(f'DELETE FROM "{doctype}" WHERE {where}', params)
1306
+ if not self._in_transaction:
1307
+ self.conn.commit()
1308
+
1309
+ def commit(self):
1310
+ with self._lock:
1311
+ self.conn.commit()
1312
+
1313
+ def rollback(self):
1314
+ self.conn.rollback()
1315
+
1316
+ def close(self):
1317
+ self.conn.close()
1318
+
1319
+
1320
+ # Module-level singleton, initialized by setup()
1321
+ # -----------------------------------------------------------------
1322
+ # Schema migrations
1323
+ #
1324
+ # Each tuple: (version: int, name: str, callable(db: Database) -> None).
1325
+ # Numbers are monotonic — never renumber or remove applied migrations.
1326
+ # Add new migrations by appending with the next integer.
1327
+ # -----------------------------------------------------------------
1328
+
1329
+ def _m001_chat_message_session_id(db: "Database") -> None:
1330
+ db._add_column_if_missing("Chat Message", "session_id", "TEXT")
1331
+
1332
+
1333
+ def _m002_chat_session_user_id(db: "Database") -> None:
1334
+ db._add_column_if_missing("Chat Session", "user_id", "TEXT")
1335
+
1336
+
1337
+ def _m003_transactional_remarks(db: "Database") -> None:
1338
+ for table in ("Quotation", "Sales Order", "Purchase Order"):
1339
+ db._add_column_if_missing(table, "remarks", "TEXT")
1340
+
1341
+
1342
+ def _m004_master_disabled_flag(db: "Database") -> None:
1343
+ for table in ("Company", "Cost Center", "Customer", "Supplier", "Item", "Warehouse"):
1344
+ db._add_column_if_missing(table, "disabled", "INTEGER DEFAULT 0")
1345
+
1346
+
1347
+ def _m005_company_stock_in_hand_account(db: "Database") -> None:
1348
+ # The field existed in code (purchase_receipt.py / delivery_note.py) but
1349
+ # not in the schema. Without it, GL entries were silently skipped because
1350
+ # SQLite's quoted-identifier quirk returns None instead of erroring.
1351
+ db._add_column_if_missing("Company", "stock_in_hand_account", "TEXT")
1352
+
1353
+
1354
+ def _m006_company_opening_balance_equity(db: "Database") -> None:
1355
+ # Added so opening-stock (and in future other opening-balance) flows have
1356
+ # a dedicated contra instead of distorting Stock Adjustment or SRBNB.
1357
+ db._add_column_if_missing("Company", "default_opening_balance_equity", "TEXT")
1358
+
1359
+
1360
+ def _m007_pos_invoice_return_fields(db: "Database") -> None:
1361
+ # POS Invoice had no return flow; add the fields that mirror Sales
1362
+ # Invoice so make_pos_return can record is_return/return_against.
1363
+ db._add_column_if_missing("POS Invoice", "is_return", "INTEGER DEFAULT 0")
1364
+ db._add_column_if_missing("POS Invoice", "return_against", "TEXT")
1365
+
1366
+
1367
+ def _m008_report_draft_source_chat_session_id(db: "Database") -> None:
1368
+ db._add_column_if_missing("Report Draft", "source_chat_session_id", "TEXT")
1369
+
1370
+
1371
+ def _m009_company_charge_accounts(db: "Database") -> None:
1372
+ """Add the two standard charge accounts (Freight In, Customs & Duties)
1373
+ to every existing company and wire them onto the Company defaults.
1374
+
1375
+ For fresh companies going forward this is handled in
1376
+ `setup_chart_of_accounts`. This migration backfills on-disk DBs that
1377
+ predate the charge-account work so the LLM / UI has somewhere to
1378
+ route supplier-invoice freight and customs charges.
1379
+ """
1380
+ db._add_column_if_missing("Company", "default_freight_in_account", "TEXT")
1381
+ db._add_column_if_missing("Company", "default_customs_account", "TEXT")
1382
+
1383
+ companies = db.conn.execute('SELECT name FROM "Company"').fetchall()
1384
+ for row in companies:
1385
+ company = row[0]
1386
+ abbr = company[:4].upper()
1387
+ op_ex = f"Operating Expenses - {abbr}"
1388
+ op_ex_exists = db.conn.execute(
1389
+ 'SELECT 1 FROM "Account" WHERE name = ?', [op_ex]
1390
+ ).fetchone()
1391
+ if not op_ex_exists:
1392
+ # Odd shape — skip silently rather than crash the migration.
1393
+ continue
1394
+
1395
+ for label, acct_type in (("Freight In", "Chargeable"),
1396
+ ("Customs & Duties", "Chargeable")):
1397
+ acct_name = f"{label} - {abbr}"
1398
+ exists = db.conn.execute(
1399
+ 'SELECT 1 FROM "Account" WHERE name = ?', [acct_name]
1400
+ ).fetchone()
1401
+ if exists:
1402
+ continue
1403
+ db.conn.execute(
1404
+ 'INSERT INTO "Account" (name, account_name, parent_account, '
1405
+ 'company, root_type, report_type, account_type, '
1406
+ 'account_currency, is_group) '
1407
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)',
1408
+ [acct_name, label, op_ex, company, "Expense",
1409
+ "Profit and Loss", acct_type,
1410
+ db.conn.execute(
1411
+ 'SELECT default_currency FROM "Company" WHERE name = ?',
1412
+ [company],
1413
+ ).fetchone()[0] or "USD"],
1414
+ )
1415
+
1416
+ db.conn.execute(
1417
+ 'UPDATE "Company" SET '
1418
+ 'default_freight_in_account = COALESCE(default_freight_in_account, ?), '
1419
+ 'default_customs_account = COALESCE(default_customs_account, ?) '
1420
+ 'WHERE name = ?',
1421
+ [f"Freight In - {abbr}", f"Customs & Duties - {abbr}", company],
1422
+ )
1423
+
1424
+
1425
+ def _m010_master_zip_code(db: "Database") -> None:
1426
+ # Postal code is free text — values like "8400", "ZH 8400" and "59123" are
1427
+ # all valid — so it's TEXT, never numeric.
1428
+ for table in ("Company", "Customer", "Supplier", "Warehouse"):
1429
+ db._add_column_if_missing(table, "zip_code", "TEXT")
1430
+ # Warehouse never had an address block in the schema even though the form
1431
+ # exposes an Address field; add it so warehouse address/city/country/zip
1432
+ # actually persist (set_value would otherwise error on the missing columns).
1433
+ for col in ("address", "city", "country"):
1434
+ db._add_column_if_missing("Warehouse", col, "TEXT")
1435
+
1436
+
1437
+ def _m011_payment_entry_currency(db: "Database") -> None:
1438
+ # Payment Entry gains a currency + conversion_rate so it can settle a
1439
+ # foreign-currency invoice and post realized FX gain/loss.
1440
+ db._add_column_if_missing("Payment Entry", "currency", "TEXT DEFAULT 'USD'")
1441
+ db._add_column_if_missing("Payment Entry", "conversion_rate", "REAL DEFAULT 1.0")
1442
+
1443
+
1444
+ def _m012_exchange_gain_loss_account(db: "Database") -> None:
1445
+ """Add the Exchange Gain/Loss account to every existing company and wire it
1446
+ onto Company.default_exchange_gain_loss_account. Mirrors the charge-account
1447
+ backfill (_m009); fresh companies get it via setup_chart_of_accounts.
1448
+ """
1449
+ db._add_column_if_missing("Company", "default_exchange_gain_loss_account", "TEXT")
1450
+
1451
+ companies = db.conn.execute('SELECT name FROM "Company"').fetchall()
1452
+ for row in companies:
1453
+ company = row[0]
1454
+ abbr = company[:4].upper()
1455
+ op_ex = f"Operating Expenses - {abbr}"
1456
+ if not db.conn.execute('SELECT 1 FROM "Account" WHERE name = ?', [op_ex]).fetchone():
1457
+ continue
1458
+
1459
+ acct_name = f"Exchange Gain/Loss - {abbr}"
1460
+ if not db.conn.execute('SELECT 1 FROM "Account" WHERE name = ?', [acct_name]).fetchone():
1461
+ currency = db.conn.execute(
1462
+ 'SELECT default_currency FROM "Company" WHERE name = ?', [company]
1463
+ ).fetchone()[0] or "USD"
1464
+ db.conn.execute(
1465
+ 'INSERT INTO "Account" (name, account_name, parent_account, company, '
1466
+ 'root_type, report_type, account_type, account_currency, is_group) '
1467
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)',
1468
+ [acct_name, "Exchange Gain/Loss", op_ex, company, "Expense",
1469
+ "Profit and Loss", "", currency],
1470
+ )
1471
+
1472
+ db.conn.execute(
1473
+ 'UPDATE "Company" SET default_exchange_gain_loss_account = '
1474
+ 'COALESCE(default_exchange_gain_loss_account, ?) WHERE name = ?',
1475
+ [acct_name, company],
1476
+ )
1477
+
1478
+
1479
+ def _m013_unrealized_exchange_account(db: "Database") -> None:
1480
+ """Add the Unrealized Exchange Gain/Loss account to every existing company
1481
+ and wire Company.default_unrealized_exchange_account. Mirrors _m012."""
1482
+ db._add_column_if_missing("Company", "default_unrealized_exchange_account", "TEXT")
1483
+
1484
+ companies = db.conn.execute('SELECT name FROM "Company"').fetchall()
1485
+ for row in companies:
1486
+ company = row[0]
1487
+ abbr = company[:4].upper()
1488
+ op_ex = f"Operating Expenses - {abbr}"
1489
+ if not db.conn.execute('SELECT 1 FROM "Account" WHERE name = ?', [op_ex]).fetchone():
1490
+ continue
1491
+
1492
+ acct_name = f"Unrealized Exchange Gain/Loss - {abbr}"
1493
+ if not db.conn.execute('SELECT 1 FROM "Account" WHERE name = ?', [acct_name]).fetchone():
1494
+ currency = db.conn.execute(
1495
+ 'SELECT default_currency FROM "Company" WHERE name = ?', [company]
1496
+ ).fetchone()[0] or "USD"
1497
+ db.conn.execute(
1498
+ 'INSERT INTO "Account" (name, account_name, parent_account, company, '
1499
+ 'root_type, report_type, account_type, account_currency, is_group) '
1500
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)',
1501
+ [acct_name, "Unrealized Exchange Gain/Loss", op_ex, company, "Expense",
1502
+ "Profit and Loss", "", currency],
1503
+ )
1504
+
1505
+ db.conn.execute(
1506
+ 'UPDATE "Company" SET default_unrealized_exchange_account = '
1507
+ 'COALESCE(default_unrealized_exchange_account, ?) WHERE name = ?',
1508
+ [acct_name, company],
1509
+ )
1510
+
1511
+
1512
+ Database.MIGRATIONS = [
1513
+ (1, "chat_message_session_id", _m001_chat_message_session_id),
1514
+ (2, "chat_session_user_id", _m002_chat_session_user_id),
1515
+ (3, "transactional_remarks", _m003_transactional_remarks),
1516
+ (4, "master_disabled_flag", _m004_master_disabled_flag),
1517
+ (5, "company_stock_in_hand_account", _m005_company_stock_in_hand_account),
1518
+ (6, "company_opening_balance_equity", _m006_company_opening_balance_equity),
1519
+ (7, "pos_invoice_return_fields", _m007_pos_invoice_return_fields),
1520
+ (8, "report_draft_source_chat_session_id", _m008_report_draft_source_chat_session_id),
1521
+ (9, "company_charge_accounts", _m009_company_charge_accounts),
1522
+ (10, "master_zip_code", _m010_master_zip_code),
1523
+ (11, "payment_entry_currency", _m011_payment_entry_currency),
1524
+ (12, "exchange_gain_loss_account", _m012_exchange_gain_loss_account),
1525
+ (13, "unrealized_exchange_account", _m013_unrealized_exchange_account),
1526
+ ]
1527
+
1528
+
1529
+ _db = None
1530
+
1531
+
1532
+ def get_db():
1533
+ global _db
1534
+ if _db is None:
1535
+ _db = Database()
1536
+ return _db
1537
+
1538
+
1539
+ def setup(db_path=":memory:"):
1540
+ """Initialize the database. Call once at startup."""
1541
+ global _db
1542
+ _db = Database(db_path)
1543
+ return _db