invoiceum 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Asinerum Conlang Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: invoiceum
3
+ Version: 1.0.0
4
+ Summary: Literp Invoicing Web Tool
5
+ Home-page: https://github.com/asinerum/invoiceum
6
+ Author: Asinerum Conlang Project
7
+ Author-email: asinerum.com@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: tagrepo>=1.0.15
16
+ Requires-Dist: orderum>=1.0.1
17
+ Dynamic: license-file
18
+
19
+ # Literp Invoicing Tool
20
+
21
+ ## Installation
22
+ ```bash
23
+ pip install invoiceum
24
+ ```
25
+
26
+ ## Sample Usage [Nov 1, 2026]
27
+ ```bash
28
+ ## nano invoiceum.sh
29
+ export LLM_TRANSFORMER_MODEL="./all-MiniLM-L6-v2"
30
+ export SETUPKEY_ENCRYPT_SECRET="very-strong-password"
31
+ export JWT_SECRET="very-strong-password"
32
+ export TOKEN_EXPIRE_MINUTES=30
33
+ export ADMIN_AUTHENTICATOR_EMAIL_LIST=someone@company.com
34
+ export SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
35
+ export STAFF_AUTHENTICATOR_EMAIL_LIST=""
36
+ export FINANCE_SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
37
+ export FINANCE_STAFF_AUTHENTICATOR_EMAIL_LIST=""
38
+ export FILE_AVRO_DATA="./users/setupkeys.avro"
39
+ export FILE_AVRO_FINANCE_SUPERVISORS="./users/rolefbksupervisors.avro"
40
+ export FILE_AVRO_FINANCE_STAFF="./users/rolefbkstaff.avro"
41
+ export FILE_AVRO_SUPERVISORS="./users/rolecrmsupervisors.avro"
42
+ export FILE_AVRO_STAFF="./users/rolecrmstaff.avro"
43
+ export FILE_DUCKDB_VECTOR="./temp.db"
44
+ uvia -a vapp -m invoiceum [-p <PORT>] [-H <HOST>]
45
+ ## The <PORT> is 21911 by default, but can be edited
46
+ ## The <HOST> should be 127.0.0.1 for security reason
47
+ ## chmod +x invoiceum.sh
48
+ ## ./invoiceum.sh
49
+ ```
50
+
51
+ Detailed tips, tricks, and examples, can be found at project's repository
52
+ https://github.com/asinerum/invoiceum
53
+
54
+ (C) 2026 Asinerum Conlang Project
@@ -0,0 +1,36 @@
1
+ # Literp Invoicing Tool
2
+
3
+ ## Installation
4
+ ```bash
5
+ pip install invoiceum
6
+ ```
7
+
8
+ ## Sample Usage [Nov 1, 2026]
9
+ ```bash
10
+ ## nano invoiceum.sh
11
+ export LLM_TRANSFORMER_MODEL="./all-MiniLM-L6-v2"
12
+ export SETUPKEY_ENCRYPT_SECRET="very-strong-password"
13
+ export JWT_SECRET="very-strong-password"
14
+ export TOKEN_EXPIRE_MINUTES=30
15
+ export ADMIN_AUTHENTICATOR_EMAIL_LIST=someone@company.com
16
+ export SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
17
+ export STAFF_AUTHENTICATOR_EMAIL_LIST=""
18
+ export FINANCE_SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
19
+ export FINANCE_STAFF_AUTHENTICATOR_EMAIL_LIST=""
20
+ export FILE_AVRO_DATA="./users/setupkeys.avro"
21
+ export FILE_AVRO_FINANCE_SUPERVISORS="./users/rolefbksupervisors.avro"
22
+ export FILE_AVRO_FINANCE_STAFF="./users/rolefbkstaff.avro"
23
+ export FILE_AVRO_SUPERVISORS="./users/rolecrmsupervisors.avro"
24
+ export FILE_AVRO_STAFF="./users/rolecrmstaff.avro"
25
+ export FILE_DUCKDB_VECTOR="./temp.db"
26
+ uvia -a vapp -m invoiceum [-p <PORT>] [-H <HOST>]
27
+ ## The <PORT> is 21911 by default, but can be edited
28
+ ## The <HOST> should be 127.0.0.1 for security reason
29
+ ## chmod +x invoiceum.sh
30
+ ## ./invoiceum.sh
31
+ ```
32
+
33
+ Detailed tips, tricks, and examples, can be found at project's repository
34
+ https://github.com/asinerum/invoiceum
35
+
36
+ (C) 2026 Asinerum Conlang Project
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,31 @@
1
+ [metadata]
2
+ name = invoiceum
3
+ version = 1.0.0
4
+ author = Asinerum Conlang Project
5
+ author_email = asinerum.com@gmail.com
6
+ description = Literp Invoicing Web Tool
7
+ long_description = file: README.md
8
+ long_description_content_type = text/markdown
9
+ url = https://github.com/asinerum/invoiceum
10
+ license = MIT
11
+ classifiers =
12
+ Programming Language :: Python :: 3
13
+ License :: OSI Approved :: MIT License
14
+ Operating System :: OS Independent
15
+
16
+ [options]
17
+ package_dir =
18
+ = src
19
+ packages = find:
20
+ python_requires = >=3.7
21
+ install_requires =
22
+ tagrepo >= 1.0.15
23
+ orderum >= 1.0.1
24
+
25
+ [options.packages.find]
26
+ where = src
27
+
28
+ [egg_info]
29
+ tag_build =
30
+ tag_date = 0
31
+
@@ -0,0 +1,3 @@
1
+ from .cinvoice import *
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,251 @@
1
+ """
2
+ Literp Invoicing Tool
3
+ Dependencies:
4
+ pip install [tagrepo] uvia vapp invoiceum
5
+ Start daemon:
6
+ export LLM_TRANSFORMER_MODEL="./all-MiniLM-L6-v2"
7
+ export SETUPKEY_ENCRYPT_SECRET="very-strong-password"
8
+ export JWT_SECRET="very-strong-password"
9
+ export TOKEN_EXPIRE_MINUTES=30
10
+ export ADMIN_AUTHENTICATOR_EMAIL_LIST=someone@company.com
11
+ export SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
12
+ export STAFF_AUTHENTICATOR_EMAIL_LIST=""
13
+ export FINANCE_SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=""
14
+ export FINANCE_STAFF_AUTHENTICATOR_EMAIL_LIST=""
15
+ export FILE_AVRO_DATA="./users/setupkeys.avro"
16
+ export FILE_AVRO_FINANCE_SUPERVISORS="./users/rolefbksupervisors.avro"
17
+ export FILE_AVRO_FINANCE_STAFF="./users/rolefbkstaff.avro"
18
+ export FILE_AVRO_SUPERVISORS="./users/rolecrmsupervisors.avro"
19
+ export FILE_AVRO_STAFF="./users/rolecrmstaff.avro"
20
+ export FILE_DUCKDB_VECTOR="./temp.db"
21
+ uvia -a vapp -m invoiceum [-p <PORT>] [-H <HOST>]
22
+ VAPP routes:
23
+ Route: /callback -> callbackpage()
24
+ Route: /login -> loginpage()
25
+ Route: /call -> callpage()
26
+ GET -> request.query_params = {function, access_token, jsondata, module}
27
+ """
28
+
29
+ from tagrepo.format import first_date_of_current_year, first_date_of_current_month
30
+
31
+ from orderum import *
32
+
33
+ from .param import get_request_invoice_options, get_request_payment_options, get_request_tax_options
34
+
35
+ from .task import *
36
+ from .title import *
37
+
38
+ APP_PORT = 21911 ## for <uvia> and <vapp>
39
+
40
+ ## core <uvia> functions
41
+
42
+ def homepage():
43
+ async def submit(request: Request):
44
+ return HTMLResponse(Template(APP_TMP_DIRECTORY_MENU).render(
45
+ APP_CHARSET=APP_CHARSET,
46
+ APP_VIEWPORT=APP_VIEWPORT,
47
+ APP_TITLE=APP_DESC,
48
+ FORM_TITLE=FORM_TITLE,
49
+ directory_items=[
50
+ {"href": "/login", "name": LABEL_MENU_LOGIN},
51
+ {"href": call_function(FUNCTION_INVOICE_INSERT), "name": LABEL_INVOICE_INSERT},
52
+ {"href": call_function(FUNCTION_DRAFT_INVOICES_SELECT), "name": LABEL_DRAFT_INVOICES_SELECT},
53
+ {"href": call_function(FUNCTION_NO_DRAFT_INVOICES_SELECT), "name": LABEL_NO_DRAFT_INVOICES_SELECT},
54
+ {"href": call_function(FUNCTION_INVOICES_REVIEW), "name": LABEL_INVOICES_REVIEW},
55
+ {"href": call_function(FUNCTION_PAYMENT_INSERT), "name": LABEL_PAYMENT_INSERT},
56
+ {"href": call_function(FUNCTION_DRAFT_PAYMENTS_SELECT), "name": LABEL_DRAFT_PAYMENTS_SELECT},
57
+ {"href": call_function(FUNCTION_NO_DRAFT_PAYMENTS_SELECT), "name": LABEL_NO_DRAFT_PAYMENTS_SELECT},
58
+ {"href": call_function(FUNCTION_PAYMENTS_REVIEW), "name": LABEL_PAYMENTS_REVIEW},
59
+ {"href": call_function(FUNCTION_TAX_COLLECT), "name": LABEL_TAX_COLLECT},
60
+ {"href": call_function(FUNCTION_INVOICES_SEARCH), "name": LABEL_INVOICES_SEARCH},
61
+ {"href": call_function("logout"), "name": LABEL_MENU_LOGOUT},
62
+ ]
63
+ ))
64
+ return submit
65
+
66
+ def callpage():
67
+ async def submit(request: Request):
68
+ return await process_function_call(request, auth_functions, HTMLResponse, _callpage)
69
+ return submit
70
+
71
+ async def _callpage(request, func: str):
72
+ if func == FUNCTION_CUSTOMERS_POPUP:
73
+ search = request.query_params.get("search")
74
+ return HTMLResponse(popup_customers_page(request, search))
75
+ elif func == FUNCTION_PRODUCTS_POPUP:
76
+ search = request.query_params.get("search")
77
+ return HTMLResponse(popup_products_page(request, search))
78
+ elif func == FUNCTION_INVOICES_POPUP:
79
+ search = request.query_params.get("search")
80
+ return HTMLResponse(popup_invoices_page(request, search))
81
+ elif func == FUNCTION_TYPE_POPUP:
82
+ return HTMLResponse(popup_type_page(request))
83
+ elif func == FUNCTION_INVOICE_INSERT:
84
+ if request.method == 'GET':
85
+ return HTMLResponse(insert_invoice_page(request))
86
+ else:
87
+ form = await request.form()
88
+ dated = form.get(DATED) or nowdate()
89
+ due = form.get(DUE) or ""
90
+ form = inputfilter(dstrip(form), Invoices, excludes=[])
91
+ form[DATED] = dated
92
+ form[DUE] = due
93
+ insert = insert_invoice_page(request, **form)
94
+ if insert == CALLBACK: return RedirectResponseGet(url=call_function(FUNCTION_INVOICES_SELECT))
95
+ return HTMLResponse(insert)
96
+ elif func == FUNCTION_INVOICES_SELECT:
97
+ return HTMLResponse(select_invoices_page(request, **get_request_invoice_options(request)))
98
+ elif func == FUNCTION_DRAFT_INVOICES_SELECT:
99
+ return HTMLResponse(select_draft_invoices_page(request, **get_request_invoice_options(request)))
100
+ elif func == FUNCTION_NO_DRAFT_INVOICES_SELECT:
101
+ return HTMLResponse(select_no_draft_invoices_page(request, **get_request_invoice_options(request)))
102
+ elif func == FUNCTION_INVOICE_SELECT:
103
+ invoice_id = request.query_params.get("id")
104
+ return HTMLResponse(select_invoice_page(request, invoice_id))
105
+ elif func == FUNCTION_CUSTOMER_SELECT:
106
+ customer_id = request.query_params.get("id")
107
+ return HTMLResponse(select_customer_page(request, customer_id))
108
+ elif func == FUNCTION_PRODUCT_SELECT:
109
+ product_id = request.query_params.get("id")
110
+ return HTMLResponse(select_product_page(request, product_id))
111
+ elif func == FUNCTION_INVOICE_NOTE_UPDATE: ## hidden
112
+ invoice_id = request.query_params.get("id")
113
+ if request.method == 'GET':
114
+ return HTMLResponse(update_invoice_note_page(request, invoice_id))
115
+ else:
116
+ form = await request.form()
117
+ form = inputfilter(dstrip(form), Invoices, includes=UPDATE_INPUTS["Invoices"])
118
+ update = update_invoice_note_page(request, invoice_id, **form)
119
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICE_SELECT}&id={invoice_id}"))
120
+ return HTMLResponse(update)
121
+ elif func == FUNCTION_INVOICE_UPDATE: ## hidden
122
+ invoice_id = request.query_params.get("id")
123
+ if request.method == 'GET':
124
+ return HTMLResponse(update_invoice_page(request, invoice_id))
125
+ else:
126
+ form = await request.form()
127
+ form = inputfilter(dstrip(form), Invoices, includes=UPDATE_INPUTS[Invoices])
128
+ update = update_invoice_page(request, invoice_id, **form)
129
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICE_SELECT}&id={invoice_id}"))
130
+ return HTMLResponse(update)
131
+ elif func == FUNCTION_INVOICE_UPGRADE:
132
+ invoice_id = request.query_params.get("id")
133
+ if request.method == 'GET':
134
+ return HTMLResponse(upgrade_invoice_page(request, invoice_id))
135
+ else:
136
+ form = await request.form()
137
+ status = form.get(STATUS)
138
+ update = upgrade_invoice_page(request, invoice_id, status=status)
139
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICE_SELECT}&id={invoice_id}"))
140
+ return HTMLResponse(update)
141
+ elif func == FUNCTION_INVOICEITEM_INSERT:
142
+ invoice_id = request.query_params.get("invoice_id")
143
+ if request.method == 'GET':
144
+ return HTMLResponse(insert_invoiceitem_page(request, invoice_id=invoice_id))
145
+ else:
146
+ form = await request.form()
147
+ form = inputfilter(dstrip(form), Invoiceitems, excludes=[DATED])
148
+ insert = insert_invoiceitem_page(request, invoice_id=invoice_id, **form)
149
+ if insert == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICEITEMS_SELECT}&invoice_id={invoice_id}"))
150
+ return HTMLResponse(insert)
151
+ elif func == FUNCTION_INVOICEITEMS_SELECT:
152
+ invoice_id = request.query_params.get("invoice_id")
153
+ return HTMLResponse(select_invoiceitems_page(request, invoice_id))
154
+ elif func == FUNCTION_INVOICEITEM_SELECT:
155
+ invoiceitem_id = request.query_params.get("id")
156
+ return HTMLResponse(select_invoiceitem_page(request, invoiceitem_id))
157
+ elif func == FUNCTION_INVOICEITEM_UPDATE: ## hidden
158
+ invoiceitem_id = request.query_params.get("id")
159
+ if request.method == 'GET':
160
+ return HTMLResponse(update_invoiceitem_page(request, invoiceitem_id))
161
+ else:
162
+ form = await request.form()
163
+ form = inputfilter(dstrip(form), Invoiceitems, includes=UPDATE_INPUTS[Invoiceitems])
164
+ update = update_invoiceitem_page(request, invoiceitem_id, **form) ## refresh subtotal
165
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICEITEM_SELECT}&id={invoiceitem_id}"))
166
+ return HTMLResponse(update)
167
+ elif func == FUNCTION_INVOICES_REVIEW:
168
+ if request.method == 'GET':
169
+ return HTMLResponse(review_invoices_page(request))
170
+ else:
171
+ form = await request.form()
172
+ status = form.get(STATUS)
173
+ return RedirectResponseGet(url=call_function(f"{FUNCTION_INVOICES_SELECT}&status={status}"))
174
+ elif func == FUNCTION_INVOICES_SEARCH:
175
+ author = request.query_params.get("author")
176
+ if request.method == 'GET':
177
+ return HTMLResponse(search_invoices_page(request))
178
+ else:
179
+ form = await request.form()
180
+ search = form.get("search")
181
+ seek = form.get("seek")
182
+ reset = form.get("reset")
183
+ search = search_invoices_page(request, search=search, reset=boolean(reset), seek=seek, author=author)
184
+ return HTMLResponse(search)
185
+ elif func == FUNCTION_INVOICES_AUTHOR_SEARCH: ## hidden
186
+ if request.method == 'GET':
187
+ return HTMLResponse(search_invoices_author_page(request))
188
+ else:
189
+ form = await request.form()
190
+ search = form.get("search")
191
+ seek = form.get("seek")
192
+ reset = form.get("reset")
193
+ search = search_invoices_author_page(request, search=search, reset=boolean(reset), seek=seek)
194
+ return HTMLResponse(search)
195
+ elif func == FUNCTION_PAYMENT_INSERT:
196
+ if request.method == 'GET':
197
+ return HTMLResponse(insert_payment_page(request))
198
+ else:
199
+ form = await request.form()
200
+ dated = form.get(DATED) or nowdate()
201
+ form = inputfilter(dstrip(form), Payments, excludes=[])
202
+ form[DATED] = dated
203
+ insert = insert_payment_page(request, **form)
204
+ if insert == CALLBACK: return RedirectResponseGet(url=call_function(FUNCTION_PAYMENTS_SELECT))
205
+ return HTMLResponse(insert)
206
+ elif func == FUNCTION_PAYMENTS_SELECT:
207
+ return HTMLResponse(select_payments_page(request, **get_request_payment_options(request)))
208
+ elif func == FUNCTION_PAYMENT_SELECT:
209
+ payment_id = request.query_params.get("id")
210
+ return HTMLResponse(select_payment_page(request, payment_id))
211
+ elif func == FUNCTION_PAYMENT_UPDATE: ## hidden
212
+ payment_id = request.query_params.get("id")
213
+ if request.method == 'GET':
214
+ return HTMLResponse(update_payment_page(request, payment_id))
215
+ else:
216
+ form = await request.form()
217
+ form = inputfilter(dstrip(form), Payments, includes=UPDATE_INPUTS[Payments])
218
+ update = update_payment_page(request, payment_id, **form)
219
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_PAYMENT_SELECT}&id={payment_id}"))
220
+ return HTMLResponse(update)
221
+ elif func == FUNCTION_PAYMENT_UPGRADE:
222
+ payment_id = request.query_params.get("id")
223
+ if request.method == 'GET':
224
+ return HTMLResponse(upgrade_payment_page(request, payment_id))
225
+ else:
226
+ form = await request.form()
227
+ status = form.get(STATUS)
228
+ update = upgrade_payment_page(request, payment_id, status=status)
229
+ if update == CALLBACK: return RedirectResponseGet(url=call_function(f"{FUNCTION_PAYMENT_SELECT}&id={payment_id}"))
230
+ return HTMLResponse(update)
231
+ elif func == FUNCTION_DRAFT_PAYMENTS_SELECT:
232
+ return HTMLResponse(select_draft_payments_page(request, **get_request_payment_options(request)))
233
+ elif func == FUNCTION_NO_DRAFT_PAYMENTS_SELECT:
234
+ return HTMLResponse(select_no_draft_payments_page(request, **get_request_payment_options(request)))
235
+ elif func == FUNCTION_PAYMENTS_REVIEW:
236
+ if request.method == 'GET':
237
+ return HTMLResponse(review_payments_page(request))
238
+ else:
239
+ form = await request.form()
240
+ status = form.get(STATUS)
241
+ return RedirectResponseGet(url=call_function(f"{FUNCTION_PAYMENTS_SELECT}&status={status}"))
242
+ elif func == FUNCTION_TAX_COLLECT:
243
+ if request.method == 'GET':
244
+ return HTMLResponse(collect_tax_invoices_page(request))
245
+ else:
246
+ form = await request.form()
247
+ dated = form.get(DATED) or first_date_of_current_month()
248
+ dated_end = form.get(DATED_END) or nowdate()
249
+ return HTMLResponse(collect_tax_invoices_page(request, dated, dated_end))
250
+ else:
251
+ return HTMLResponse("Function not found or not accessible")
@@ -0,0 +1,117 @@
1
+ from tagrepo.dataframe import _get_row_html, _get_row_dict
2
+ from tagrepo.database import avro_sql_select_combo
3
+
4
+ from orderum.data import *
5
+
6
+ from .syslogin import *
7
+ from .environ import *
8
+ from .schema import *
9
+
10
+ ## data constants
11
+
12
+ INVOICE_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Invoices], excludes=[])
13
+ ## EX_INVOICE_INSERT_FIELDS = arrayfilter(["customer"]+INSERT_INPUTS[Invoices], excludes=[])
14
+ INVOICEITEM_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Invoiceitems], excludes=[INVOICE_ID, DATED, SUBTOTAL])
15
+ ## EX_INVOICEITEM_INSERT_FIELDS = arrayfilter(["product"]+INSERT_INPUTS[Invoiceitems], excludes=[INVOICE_ID, DATED, SUBTOTAL])
16
+ PAYMENT_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Payments], excludes=[])
17
+
18
+ ## selecting derivatives
19
+
20
+ def calculate_invoice_amount(invoice_id: str) -> float:
21
+ df = sql_simple_query_model("Invoiceitems", select="quantity,price,subtotal", where=f"invoice_id='{invoice_id}'")
22
+ return 0 if df is None or not len(df) else numfloat((df['quantity'] * df['price']).sum())
23
+
24
+ def get_invoice_name(invoice_id: str) -> str:
25
+ return select_name_by_id(invoice_id, model="Invoices")
26
+
27
+ def get_invoice(invoice_id: str) -> dict:
28
+ return _get_row_dict("Invoices", invoice_id)
29
+
30
+ def select_pure_invoice(invoice_id: str) -> str:
31
+ return _get_row_html("Invoices", invoice_id)
32
+
33
+ def select_invoice(invoice_id: str) -> str:
34
+ return select_pure_row("Invoices",
35
+ linkfields=SELECT_LINK_FIELDS[Invoices],
36
+ linkrefs={CUSTOMER_ID: call_function(f"{FUNCTION_CUSTOMER_SELECT}&id=")},
37
+ where=f"id='{invoice_id}'", limit=1)
38
+
39
+ def select_invoiceitem(invoiceitem_id: str) -> str:
40
+ return select_pure_row("Invoiceitems",
41
+ linkfields=SELECT_LINK_FIELDS[Invoiceitems],
42
+ linkrefs={INVOICE_ID: call_function(f"{FUNCTION_INVOICE_SELECT}&id="), PRODUCT_ID: call_function(f"{FUNCTION_PRODUCT_SELECT}&id=")},
43
+ where=f"id='{invoiceitem_id}'", limit=1)
44
+
45
+ def get_invoiceitem(invoiceitem_id: str) -> dict:
46
+ return _get_row_dict("Invoiceitems", invoiceitem_id)
47
+
48
+ def invoice_not_updatable(invoice: any) -> bool:
49
+ if type(invoice) is dict: return invoice.get(STATUS) not in UPDATABLE_INVOICES
50
+ return not check_record_exist(invoice, model="Invoices", field=ID, where=INVOICE_IS_UPDATABLE)
51
+
52
+ def invoice_closed(invoice: any) -> bool:
53
+ if type(invoice) is dict: return invoice.get(STATUS) in CLOSED_INVOICES
54
+ return check_record_exist(invoice, model="Invoices", field=ID, where=INVOICE_CLOSED)
55
+
56
+ def invoice_item_duplicated(name: str, invoice_id: str) -> bool:
57
+ return check_record_exist(name, model="Invoiceitems", field=NAME, where=f"invoice_id='{invoice_id}'")
58
+
59
+ def select_payment(payment_id: str) -> str:
60
+ return select_pure_row("Payments",
61
+ linkfields=SELECT_LINK_FIELDS[Payments],
62
+ linkrefs={INVOICE_ID: call_function(f"{FUNCTION_INVOICE_SELECT}&id=")},
63
+ where=f"id='{payment_id}'", limit=1)
64
+
65
+ def get_payment(payment_id: str) -> dict:
66
+ return _get_row_dict("Payments", payment_id)
67
+
68
+ def payment_not_updatable(payment: any) -> bool:
69
+ if type(payment) is dict: return payment.get(STATUS) not in UPDATABLE_PAYMENTS
70
+ return not check_record_exist(payment, model="Payments", field=ID, where=PAYMENT_IS_UPDATABLE)
71
+
72
+ def payment_closed(payment: any) -> bool:
73
+ if type(payment) is dict: return payment.get(STATUS) in CLOSED_PAYMENTS
74
+ return check_record_exist(payment, model="Payments", field=ID, where=PAYMENT_CLOSED)
75
+
76
+ ## listing derivatives
77
+
78
+ def list_invoices(popindex: int=1, **kwargs) -> str:
79
+ model = "Invoices"
80
+ show = LISTING_FIELDS[Invoices][popindex:]
81
+ df = sql_simple_query_model(model, **kwargs)
82
+ return df_list_insert_rows(df, show,
83
+ header=LISTING_HEADERS[Invoices][popindex:],
84
+ open_func=LISTING_LINK_BUTTON_FUNCTIONS[Invoices][popindex:],
85
+ callfunction=LISTING_LINK_CALL_FUNCTIONS[Invoices][popindex:],
86
+ idfield=LISTING_LINK_ID_FIELDS[Invoices][popindex:],
87
+ linkfield=LISTING_LINK_REF_FIELDS[Invoices][popindex:])
88
+
89
+ def list_invoices_to_insert_invoiceitem(**kwargs) -> str:
90
+ return list_invoices(0, **kwargs) ## remove add_item
91
+
92
+ def list_invoiceitems(invoice_id: str) -> str:
93
+ return list_review_rows("Invoiceitems",
94
+ show=LISTING_FIELDS[Invoiceitems],
95
+ callfunction="select-invoiceitem",
96
+ where=f"invoice_id='{invoice_id}'")
97
+
98
+ def list_pick_invoices(**kwargs) -> str:
99
+ return list_select_rows(model="Invoices", show=SELECT_LISTING_FIELDS[Invoices], header=LISTING_HEADERS["Invoices"], **kwargs) ## ambiguous "Invoices"
100
+
101
+ def list_select_payments(**kwargs) -> str:
102
+ return list_review_rows(model="Payments", show=LISTING_FIELDS[Payments], callfunction=FUNCTION_PAYMENT_SELECT, **kwargs)
103
+
104
+ list_payments = list_select_payments
105
+
106
+ def list_tax_invoices(dated: str, dated_end: str, statuses: str=INVOICE_TAXABLE_STATUSES, join_clause="db_main.customer_id=db_sub.id") -> str:
107
+ df = avro_sql_select_combo(model_main="Invoices", model_sub="Customers",
108
+ select=f"db_main.id, db_main.dated, db_sub.name AS customer, db_sub.{CUSTOMER_TAX_CODE} AS taxcode, db_main.tax, db_main.taxrate",
109
+ where=f"{join_clause} AND db_main.status IN {statuses} AND db_main.dated BETWEEN '{dated}' AND '{dated_end}'",
110
+ sort="db_main.dated,db_sub.name", asc=True, limit=LIST_MAX_ROWS)
111
+ if df is None or not len(df): return NO_DATA_AVAILABLE
112
+ return df_list_review_rows(df, show=REVIEW_LISTING_FIELDS[Invoices], callfunction=FUNCTION_INVOICE_SELECT)
113
+
114
+ ## vector search
115
+
116
+ def get_search_invoice_option(request, seek: str="", author: str="") -> dict:
117
+ return get_search_status_option(request=request, seek=seek, author=author, status_list=INVOICE_STATUS_LIST)
@@ -0,0 +1,17 @@
1
+ from orderum.environ import *
2
+
3
+ set_environ('CONFIRM_INVOICE_COMPLETED', 'Confirm: this invoice was completed?')
4
+ set_environ('CONFIRM_INVOICE_CANCELLED', 'Confirm: this invoice was cancelled?')
5
+
6
+ set_environ('WRONG_INVOICE_ID', 'Error: invalid invoice id')
7
+ set_environ('BAD_INVOICE_ID', 'Error: invalid invoice id, or invoice is not updatable')
8
+ set_environ('INVOICE_ITEM_DUPLICATED', 'Error: invoice line item is duplicated')
9
+
10
+ set_environ('INVOICE_NOT_EXIST', 'Warning: invoice does not exist')
11
+ set_environ('INVOICE_NOT_EXIST_OR_CLOSED', 'Warning: invoice does not exist or was closed')
12
+ set_environ('INVOICE_NO_REVERSE_TO_DRAFT', 'Warning: invoice cannot be reversed to draft')
13
+ set_environ('INVOICE_ALREADY_EXISTS', 'Warning: invoice with this name already exists')
14
+
15
+ set_environ('PAYMENT_ALREADY_EXISTS', 'Warning: payment with this name already exists')
16
+ set_environ('PAYMENT_NOT_EXIST_OR_CLOSED', 'Warning: payment does not exist or was closed')
17
+ set_environ('BAD_PAYMENT_ID', 'Error: invalid payment id, or payment is not updatable')
@@ -0,0 +1,18 @@
1
+ from orderum.param import *
2
+
3
+ from .schema import INVOICE_SORT_FIELDS, PAYMENT_SORT_FIELDS, TAX_SORT_FIELDS, NAME
4
+
5
+ def adjust_invoice_listing_options(where: str, author: str, sort: str, asc: str, limit: str, request=None) -> tuple:
6
+ return adjust_listing_options(where, author, sort, asc, limit, sort_fields=INVOICE_SORT_FIELDS, request=request)
7
+
8
+ def adjust_payment_listing_options(where: str, author: str, sort: str, asc: str, limit: str, request=None) -> tuple:
9
+ return adjust_listing_options(where, author, sort, asc, limit, sort_fields=PAYMENT_SORT_FIELDS, request=request)
10
+
11
+ def adjust_tax_listing_options(where: str, author: str, sort: str, asc: str, limit: str, request=None) -> tuple:
12
+ return adjust_listing_options(where, author, sort, asc, limit, sort_fields=TAX_SORT_FIELDS, request=request)
13
+
14
+ get_request_invoice_options = get_request_data_options ## sort:DATED.desc
15
+
16
+ get_request_payment_options = get_request_data_options ## sort:DATED.desc
17
+
18
+ get_request_tax_options = get_request_data_options ## sort:DATED.desc
@@ -0,0 +1,97 @@
1
+ from tagrepo.schema15 import *
2
+ from orderum.schema import *
3
+
4
+ from .syslogin import *
5
+
6
+ _CUSTOMER = "customer"
7
+ _TAXCODE = "taxcode"
8
+
9
+ INVOICE_SORT_FIELDS = [NAME, DATED, DUE, AMOUNT, DISCOUNT, TAXRATE, TAX, AUTHOR_ID, STATUS]
10
+ PAYMENT_SORT_FIELDS = [NAME, DATED, AMOUNT, REF, TYPE, AUTHOR_ID, STATUS]
11
+ TAX_SORT_FIELDS = [_CUSTOMER, _TAXCODE, TAX, TAXRATE, DATED]
12
+
13
+ UPDATABLE_INVOICES = [INVOICE_DRAFT]
14
+ CLOSED_INVOICES = [INVOICE_PAID, INVOICE_CANCELLED]
15
+ OPEN_INVOICES = [INVOICE_SENT, INVOICE_OVERDUE] ## USABLE
16
+
17
+ INVOICE_CLOSED = f"status IN ('{INVOICE_PAID}', '{INVOICE_CANCELLED}')"
18
+ INVOICE_NOT_CLOSED = f"status NOT IN ('{INVOICE_PAID}', '{INVOICE_CANCELLED}')"
19
+ INVOICE_IS_LISTABLE = f"status NOT IN ('{INVOICE_DRAFT}', '{INVOICE_CANCELLED}')"
20
+ INVOICE_IS_UPDATABLE = f"status IN ('{INVOICE_DRAFT}')"
21
+ INVOICE_IS_USABLE = f"status IN ('{INVOICE_SENT}', '{INVOICE_OVERDUE}')"
22
+
23
+ UPDATABLE_PAYMENTS = [PAYMENT_DRAFT]
24
+ CLOSED_PAYMENTS = [PAYMENT_COMPLETED, PAYMENT_CANCELLED]
25
+
26
+ PAYMENT_CLOSED = f"status IN ('{PAYMENT_COMPLETED}', '{PAYMENT_CANCELLED}')"
27
+ PAYMENT_NOT_CLOSED = f"status NOT IN ('{PAYMENT_COMPLETED}', '{PAYMENT_CANCELLED}')"
28
+ PAYMENT_IS_LISTABLE = f"status NOT IN ('{PAYMENT_DRAFT}', '{PAYMENT_CANCELLED}')"
29
+ PAYMENT_IS_UPDATABLE = f"status IN ('{PAYMENT_DRAFT}')"
30
+
31
+ INVOICE_TAXABLE_STATUSES = f"('{INVOICE_SENT}', '{INVOICE_OVERDUE}', '{INVOICE_PAID}')"
32
+
33
+ ## complex data view: use with caution
34
+
35
+ REVIEW_LISTING_FIELDS |= { ## complex view
36
+ Invoices: [HEADER_REVIEW, DATED, _CUSTOMER, _TAXCODE, TAXRATE, TAX],
37
+ }
38
+
39
+ LISTING_LINK_BUTTON_FUNCTIONS |= { ## complex view
40
+ Invoices: [add_button, view_button],
41
+ }
42
+
43
+ LISTING_LINK_CALL_FUNCTIONS |= { ## complex view
44
+ Invoices: [FUNCTION_INVOICEITEM_INSERT, FUNCTION_INVOICE_SELECT],
45
+ }
46
+
47
+ LISTING_LINK_ID_FIELDS |= { ## complex view
48
+ Invoices: [ID, ID],
49
+ }
50
+
51
+ LISTING_LINK_REF_FIELDS |= { ## complex view
52
+ Invoices: [INVOICE_ID, ID], ## add-item-line/view-item
53
+ }
54
+
55
+ ## common data frame show
56
+
57
+ LISTING_FIELDS |= { ## dataset viewer: all columns
58
+ Invoices: [HEADER_INSERT, HEADER_REVIEW, DATED, NAME, AMOUNT, DISCOUNT, DUE, TAXRATE, STATUS],
59
+ Invoiceitems: [HEADER_REVIEW, NAME, QUANTITY, PRICE, SUBTOTAL],
60
+ Payments: [HEADER_REVIEW, DATED, NAME, AMOUNT, TYPE, REF, STATUS],
61
+ }
62
+
63
+ SELECT_LINK_FIELDS |= { ## single record viewer: vertical view with links
64
+ Invoices: [CUSTOMER_ID],
65
+ Invoiceitems: [INVOICE_ID, PRODUCT_ID],
66
+ Payments: [INVOICE_ID],
67
+ }
68
+
69
+ SELECT_LISTING_FIELDS |= { ## dataset viewer for popup picker
70
+ Invoices: [HEADER_SELECT, NAME, STATUS, ID],
71
+ }
72
+
73
+ LISTING_HEADERS |= { ## dataset viewer: headers of extra columns
74
+ Invoices: [HEADER_INSERT, HEADER_REVIEW],
75
+ "Invoices": HEADER_SELECT, ## popup picker
76
+ }
77
+
78
+ ## common data input options
79
+
80
+ INSERT_INPUTS |= {
81
+ Invoices: [DATED, DUE, CUSTOMER_ID, NAME, AMOUNT, DISCOUNT, TAXRATE, ORDER_ID, NOTE],
82
+ InvoicesStatus: [ID, STATUS],
83
+ Invoiceitems: [INVOICE_ID, PRODUCT_ID, DATED, NAME, QUANTITY, PRICE, SUBTOTAL, NOTE],
84
+ Payments: [DATED, INVOICE_ID, NAME, AMOUNT, TYPE, REF, NOTE],
85
+ }
86
+
87
+ UPDATE_INPUTS |= {
88
+ Invoices: [AUTHOR_ID, NOTE, AMOUNT, DISCOUNT, TAXRATE],
89
+ "Invoices": [AUTHOR_ID, NOTE],
90
+ InvoicesStatus: [STATUS],
91
+ Invoiceitems: [NAME, QUANTITY, PRICE, NOTE],
92
+ Payments: [NAME, AUTHOR_ID, REF, TYPE, NOTE],
93
+ }
94
+
95
+ SEARCH_INPUTS |= {
96
+ Invoices: [NAME, NOTE, AUTHOR_ID],
97
+ }