invoiceum 1.0.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.
invoiceum/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cinvoice import *
2
+
3
+ __version__ = "1.0.0"
invoiceum/cinvoice.py ADDED
@@ -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")
invoiceum/data.py ADDED
@@ -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)
invoiceum/environ.py ADDED
@@ -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')
invoiceum/param.py ADDED
@@ -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
invoiceum/schema.py ADDED
@@ -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
+ }
invoiceum/syslogin.py ADDED
@@ -0,0 +1,72 @@
1
+ from orderum.syslogin import *
2
+
3
+ from tagrepo.auth import check_finance_staff_login, check_finance_supervisor_login
4
+
5
+ FUNCTION_INVOICE_INSERT = 'insert-invoice'
6
+ FUNCTION_INVOICE_SELECT = 'select-invoice'
7
+ FUNCTION_INVOICE_NOTE_UPDATE = 'note-invoice' ##RES
8
+ FUNCTION_INVOICE_UPDATE = 'update-invoice' ##RES
9
+ FUNCTION_INVOICE_UPGRADE = 'upgrade-invoice'
10
+
11
+ FUNCTION_INVOICES_SELECT = 'select-invoices'
12
+ FUNCTION_DRAFT_INVOICES_SELECT = 'select-draft-invoices'
13
+ FUNCTION_NO_DRAFT_INVOICES_SELECT = 'select-no-draft-invoices'
14
+ FUNCTION_INVOICES_SEARCH = 'search-invoices'
15
+ FUNCTION_INVOICES_REVIEW = 'review-invoices' ##HID
16
+ FUNCTION_INVOICES_UPDATE = 'update-invoices' ##HID
17
+
18
+ FUNCTION_INVOICEITEM_INSERT = 'insert-invoiceitem'
19
+ FUNCTION_INVOICEITEM_SELECT = 'select-invoiceitem'
20
+ FUNCTION_INVOICEITEM_UPDATE = 'update-invoiceitem'
21
+ FUNCTION_INVOICEITEMS_SELECT = 'select-invoiceitems'
22
+
23
+ FUNCTION_INVOICES_AUTHOR_SEARCH = 'search-author-invoices'
24
+ FUNCTION_INVOICES_GLOBAL_SELECT = 'select-global-invoices'
25
+
26
+ FUNCTION_PAYMENT_INSERT = 'insert-payment'
27
+ FUNCTION_PAYMENT_SELECT = 'select-payment'
28
+ FUNCTION_PAYMENT_UPDATE = 'update-payment'
29
+ FUNCTION_PAYMENT_UPGRADE = 'upgrade-payment' ## HID
30
+ FUNCTION_PAYMENTS_SELECT = 'select-payments'
31
+ FUNCTION_PAYMENTS_REVIEW = 'review-payments'
32
+ FUNCTION_PAYMENTS_SEARCH = 'search-payments' ## HID
33
+ FUNCTION_DRAFT_PAYMENTS_SELECT = 'select-draft-payments'
34
+ FUNCTION_NO_DRAFT_PAYMENTS_SELECT = 'select-no-draft-payments'
35
+
36
+ FUNCTION_TAX_COLLECT = 'collect-tax'
37
+ FUNCTION_TAX_AGGREGATE = 'aggregate-tax' ## HID
38
+ FUNCTION_INVOICES_POPUP = 'popup-invoices'
39
+ FUNCTION_TYPE_POPUP = 'popup-type'
40
+
41
+ auth_functions |= {
42
+ FUNCTION_INVOICE_INSERT: {"func": check_staff_login, "args": {}},
43
+ FUNCTION_INVOICE_SELECT: {"func": check_author_login, "args": {"model": "Invoices", "id": ["id"]}},
44
+ FUNCTION_INVOICE_NOTE_UPDATE: {"func": check_author_login, "args": {"model": "Invoices", "id": ["id"]}},
45
+ FUNCTION_INVOICE_UPDATE: {"func": check_author_login, "args": {"model": "Invoices", "id": ["id"]}},
46
+ FUNCTION_INVOICE_UPGRADE: {"func": check_author_login, "args": {"model": "Invoices", "id": ["id"]}},
47
+ FUNCTION_INVOICES_SELECT: {"func": check_staff_login, "args": {}},
48
+ FUNCTION_DRAFT_INVOICES_SELECT: {"func": check_staff_login, "args": {}},
49
+ FUNCTION_NO_DRAFT_INVOICES_SELECT: {"func": check_staff_login, "args": {}},
50
+ FUNCTION_INVOICES_SEARCH: {"func": check_staff_login, "args": {}},
51
+ FUNCTION_INVOICES_REVIEW: {"func": check_staff_login, "args": {}},
52
+ FUNCTION_INVOICES_UPDATE: {"func": check_staff_login, "args": {}},
53
+ FUNCTION_INVOICEITEM_INSERT: {"func": check_author_login, "args": {"model": "Invoices", "id": ["invoice_id"]}},
54
+ FUNCTION_INVOICEITEM_SELECT: {"func": check_staff_login, "args": {}},
55
+ FUNCTION_INVOICEITEM_UPDATE: {"func": check_supervisor_login, "args": {}},
56
+ FUNCTION_INVOICEITEMS_SELECT: {"func": check_author_login, "args": {"model": "Invoices", "id": ["invoice_id"]}},
57
+ FUNCTION_INVOICES_AUTHOR_SEARCH: {"func": check_staff_login, "args": {}},
58
+ FUNCTION_INVOICES_GLOBAL_SELECT: {"func": check_staff_login, "args": {}},
59
+ FUNCTION_PAYMENT_INSERT: {"func": check_finance_staff_login, "args": {}},
60
+ FUNCTION_PAYMENT_SELECT: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
61
+ FUNCTION_PAYMENT_UPDATE: {"func": check_finance_supervisor_login, "args": {}},
62
+ FUNCTION_PAYMENT_UPGRADE: {"func": check_finance_staff_login, "args": {}},
63
+ FUNCTION_PAYMENTS_SELECT: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
64
+ FUNCTION_PAYMENTS_REVIEW: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
65
+ FUNCTION_PAYMENTS_SEARCH: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
66
+ FUNCTION_DRAFT_PAYMENTS_SELECT: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
67
+ FUNCTION_NO_DRAFT_PAYMENTS_SELECT: {"func": [check_finance_staff_login, check_staff_login], "args": [{}, {}]},
68
+ FUNCTION_TAX_COLLECT: {"func": check_finance_staff_login, "args": {}},
69
+ FUNCTION_TAX_AGGREGATE: {"func": check_finance_supervisor_login, "args": {}},
70
+ FUNCTION_INVOICES_POPUP: {"func": check_finance_staff_login, "args": {}},
71
+ FUNCTION_TYPE_POPUP: {"func": check_finance_staff_login, "args": {}},
72
+ }
invoiceum/task.py ADDED
@@ -0,0 +1,301 @@
1
+ from tagrepo.database import _log_insert, _log_update
2
+ from tagrepo.format import escape_punctuation_for_title_list
3
+ from tagrepo.param import refresh_form_data_tax
4
+
5
+ from orderum.task import *
6
+
7
+ from .param import adjust_invoice_listing_options, adjust_payment_listing_options
8
+ from .template import _selecting_page_, _picking_page_
9
+
10
+ from .data import *
11
+ from .template import *
12
+
13
+ def insert_invoice_page(request, **form) -> str:
14
+ if not form.get(NAME):
15
+ return invoice_creating_page()
16
+ elif not check_record_exist(form.get(CUSTOMER_ID), model="Customers", field=ID):
17
+ return WRONG_CUSTOMER_ID
18
+ elif check_record_exist(form[NAME], model="Invoices"):
19
+ return INVOICE_ALREADY_EXISTS
20
+ elif not valid_percent(numfloat(form.get(DISCOUNT))) or not valid_percent(numfloat(form.get(TAXRATE))):
21
+ return BAD_INPUT_DATA
22
+ else:
23
+ form[AUTHOR_ID] = request.session.get(SESSION_USER)
24
+ refresh_form_data_tax(form)
25
+ insert = safe_avro_file_insert("Invoices", multi=True, **form)
26
+ if not insert: return FAILED_SAVE_DATA
27
+ _log_insert(request, form, "Invoices")
28
+ return CALLBACK
29
+
30
+ def select_invoices_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
31
+ where = INVOICE_NOT_CLOSED if status not in INVOICE_STATUS_LIST else f"status IN ('{status}')"
32
+ where, sort, asc, limit = adjust_invoice_listing_options(where, author, sort, asc, limit, request=request)
33
+ return invoices_listing_page(
34
+ title=INVOICES_LISTING_TITLE,
35
+ action=f"{FUNCTION_INVOICES_SELECT}&status={status}&author={request.session.get(SESSION_USER)}",
36
+ content=list_invoices(where=where, sort=sort, asc=asc, limit=limit))
37
+
38
+ def select_draft_invoices_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
39
+ where = INVOICE_IS_UPDATABLE
40
+ where, sort, asc, limit = adjust_invoice_listing_options(where, author, sort, asc, limit, request=request)
41
+ return draft_invoices_listing_page(
42
+ title=DRAFT_INVOICES_LISTING_TITLE,
43
+ action=f"{FUNCTION_DRAFT_INVOICES_SELECT}&author={request.session.get(SESSION_USER)}",
44
+ content=list_invoices_to_insert_invoiceitem(where=where, sort=sort, asc=asc, limit=limit))
45
+
46
+ def select_no_draft_invoices_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
47
+ where = INVOICE_IS_LISTABLE
48
+ where, sort, asc, limit = adjust_invoice_listing_options(where, author, sort, asc, limit, request=request)
49
+ return no_draft_invoices_listing_page(
50
+ title=NO_DRAFT_INVOICES_LISTING_TITLE,
51
+ action=f"{FUNCTION_NO_DRAFT_INVOICES_SELECT}&author={request.session.get(SESSION_USER)}",
52
+ content=list_invoices(where=where, sort=sort, asc=asc, limit=limit))
53
+
54
+ def select_invoice_page(request, invoice_id: str) -> str:
55
+ ## <request> is reserved
56
+ return invoice_selecting_page(
57
+ (select_invoice(invoice_id) or NO_DATA_AVAILABLE)
58
+ + html_link_buttons([call_function(f"{FUNCTION_INVOICEITEM_INSERT}&invoice_id={invoice_id}")], [LABEL_CREATE_INVOICEITEM_BUTTON], [STYLE_SUBMIT_BUTTON])
59
+ + html_link_buttons([call_function(f"{FUNCTION_INVOICEITEMS_SELECT}&invoice_id={invoice_id}")], [LABEL_SELECT_INVOICEITEM_BUTTON], [STYLE_SUBMIT_BUTTON])
60
+ + html_link_buttons([call_function(f"{FUNCTION_INVOICE_UPGRADE}&id={invoice_id}")], [LABEL_UPGRADE_INVOICE_BUTTON], [STYLE_CANCEL_BUTTON]))
61
+
62
+ def update_invoice_note_page(request, invoice_id: str, **form) -> str:
63
+ data = get_invoice(invoice_id)
64
+ if not data: return NO_DATA_AVAILABLE
65
+ if not len(form):
66
+ return invoice_updating_page(
67
+ title=item_content_title(INVOICE_UPDATING_TITLE, data[NAME]),
68
+ action=f"{FUNCTION_INVOICE_NOTE_UPDATE}&id={invoice_id}",
69
+ content=dictfilter(data, UPDATE_INPUTS["Invoices"])) ## notes updated only
70
+ update = safe_avro_file_update("Invoices", id=invoice_id, **form)
71
+ if not update: return FAILED_SAVE_DATA
72
+ _log_update(request, form, "Invoices", id=invoice_id)
73
+ return CALLBACK
74
+
75
+ def update_invoice_page(request, invoice_id: str, **form) -> str:
76
+ data = get_invoice(invoice_id)
77
+ if not data: return NO_DATA_AVAILABLE
78
+ if invoice_not_updatable(data): return BAD_INVOICE_ID
79
+ if not len(form):
80
+ return invoice_updating_page(
81
+ title=item_content_title(INVOICE_UPDATING_TITLE, data[NAME]),
82
+ action=f"{FUNCTION_INVOICE_UPDATE}&id={invoice_id}",
83
+ content=dictfilter(data, UPDATE_INPUTS[Invoices]))
84
+ refresh_form_data_tax(form)
85
+ update = safe_avro_file_update("Invoices", id=invoice_id, **form)
86
+ if not update: return FAILED_SAVE_DATA
87
+ _log_update(request, form, "Invoices", id=invoice_id)
88
+ return CALLBACK
89
+
90
+ def upgrade_invoice_page(request, invoice_id: str, status: str=None) -> str:
91
+ if not status:
92
+ data = get_invoice(invoice_id)
93
+ if not data: return NO_DATA_AVAILABLE
94
+ return invoice_upgrading_page(
95
+ title=item_content_title(INVOICE_UPDATING_TITLE, data[NAME]),
96
+ current_status=data[STATUS])
97
+ upgrade = update_close_invoice_page(request, invoice_id, complete_status=status)
98
+ return upgrade if upgrade not in [DONE_SAVE_DATA] else CALLBACK
99
+
100
+ def update_close_invoice_page(request, invoice_id: str, complete_status: str=None) -> str:
101
+ data = get_invoice(invoice_id)
102
+ if not data: return NO_DATA_AVAILABLE
103
+ if invoice_closed(data): return INVOICE_NOT_EXIST_OR_CLOSED
104
+ if complete_status not in INVOICE_STATUS_LIST:
105
+ status = INVOICE_CANCELLED
106
+ else:
107
+ status = complete_status
108
+ if status in UPDATABLE_INVOICES: return INVOICE_NO_REVERSE_TO_DRAFT
109
+ amount = calculate_invoice_amount(invoice_id=invoice_id)
110
+ data[AMOUNT] = amount
111
+ refresh_form_data_tax(data)
112
+ tax = data[TAX]
113
+ update = safe_avro_file_update("Invoices", id=invoice_id, status=status, amount=amount, tax=tax)
114
+ if not update: return FAILED_SAVE_DATA
115
+ insert = safe_avro_file_insert("InvoicesStatus", multi=False, id=invoice_id, status=status)
116
+ if not insert: return FAILED_COMMIT_TRANSACTION
117
+ return DONE_SAVE_DATA ## dont edit this return
118
+
119
+ ## short form of <orderum.select_product_page>
120
+ def select_product_page(request, product_id: str) -> str:
121
+ return product_selecting_page(select_product(product_id) or NO_DATA_AVAILABLE)
122
+
123
+ def insert_invoiceitem_page(request, invoice_id: str, **form) -> str:
124
+ if invoice_not_updatable(invoice_id): return BAD_INVOICE_ID
125
+ if not form.get(NAME):
126
+ invoice_name = get_invoice_name(invoice_id)
127
+ return invoiceitem_creating_page(
128
+ title=sub_item_content_title(INVOICEITEMS_CREATING_TITLE, invoice_name, f"{FUNCTION_INVOICE_SELECT}&id={invoice_id}"),
129
+ action=f"{FUNCTION_INVOICEITEM_INSERT}&invoice_id={invoice_id}")
130
+ elif invoice_item_duplicated(form.get(NAME), invoice_id):
131
+ return INVOICE_ITEM_DUPLICATED
132
+ else:
133
+ form[INVOICE_ID] = invoice_id
134
+ refresh_form_data_subtotal(form)
135
+ insert = safe_avro_file_insert("Invoiceitems", multi=True, **form)
136
+ if not insert: return FAILED_SAVE_DATA
137
+ _log_insert(request, form, "Invoiceitems")
138
+ return CALLBACK
139
+
140
+ def select_invoiceitems_page(request, invoice_id: str) -> str:
141
+ ## <request> is reserved
142
+ if valid_uuid(invoice_id):
143
+ invoice_name = get_invoice_name(invoice_id)
144
+ if not invoice_name: return INVOICE_NOT_EXIST
145
+ return invoiceitems_listing_page(
146
+ title=sub_item_content_title(INVOICEITEMS_LISTING_TITLE, invoice_name, f"{FUNCTION_INVOICE_SELECT}&id={invoice_id}"),
147
+ action=f"{FUNCTION_INVOICEITEM_INSERT}&invoice_id={invoice_id}",
148
+ content=list_invoiceitems(invoice_id))
149
+ return MISSING_INPUT_DATA
150
+
151
+ def select_invoiceitem_page(request, invoiceitem_id: str) -> str:
152
+ ## <request> is reserved
153
+ return invoiceitem_selecting_page(select_invoiceitem(invoiceitem_id) or NO_DATA_AVAILABLE)
154
+
155
+ def update_invoiceitem_page(request, invoiceitem_id: str, **form) -> str:
156
+ data = get_invoiceitem(invoiceitem_id)
157
+ if not data: return NO_DATA_AVAILABLE
158
+ if invoice_not_updatable(data[INVOICE_ID]): return BAD_INVOICE_ID
159
+ if not len(form):
160
+ invoice_name = get_invoice_name(data[INVOICE_ID])
161
+ return invoiceitem_updating_page(
162
+ title=sub_item_content_title(INVOICEITEM_UPDATING_TITLE, invoice_name, f"{FUNCTION_INVOICE_SELECT}&id={data[INVOICE_ID]}"),
163
+ action=f"{FUNCTION_INVOICEITEM_UPDATE}&id={invoiceitem_id}",
164
+ content=dictfilter(data, UPDATE_INPUTS[Invoiceitems]))
165
+ refresh_form_data_subtotal(form)
166
+ update = safe_avro_file_update("Invoiceitems", id=invoiceitem_id, **form)
167
+ if not update: return FAILED_SAVE_DATA
168
+ _log_update(request, form, "Invoiceitems", id=invoiceitem_id)
169
+ return CALLBACK
170
+
171
+ def review_invoices_page(request) -> str:
172
+ ## <request> is reserved
173
+ return invoices_reviewing_page(INVOICES_LISTING_TITLE)
174
+
175
+ def search_invoices_page(request, search: str=None, reset=False, seek: str="", author: str="") -> str:
176
+ if not search:
177
+ return invoices_searching_page()
178
+ return invoices_fetching_page(request, search, reset=reset, seek=seek, author=author)
179
+
180
+ def search_invoices_author_page(request, search: str=None, reset=False, seek: str="") -> str:
181
+ if not search:
182
+ return invoices_author_searching_page()
183
+ return invoices_author_fetching_page(request, search, reset=reset, seek=seek)
184
+
185
+ def insert_payment_page(request, **form) -> str:
186
+ if not form.get(NAME) or numfloat(form.get(AMOUNT)) <= 0:
187
+ return payment_creating_page()
188
+ elif not check_record_exist(form.get(INVOICE_ID), model="Invoices", field=ID):
189
+ return WRONG_INVOICE_ID
190
+ elif check_record_exist(form[NAME], model="Payments"):
191
+ return PAYMENT_ALREADY_EXISTS
192
+ else:
193
+ form[AUTHOR_ID] = request.session.get(SESSION_USER)
194
+ insert = safe_avro_file_insert("Payments", multi=True, **form)
195
+ if not insert: return FAILED_SAVE_DATA
196
+ _log_insert(request, form, "Payments")
197
+ return CALLBACK
198
+
199
+ def compact_select_invoices_page() -> str:
200
+ return _selecting_page_(INVOICES_LISTING_TITLE,
201
+ list_pick_invoices(where=INVOICE_IS_USABLE, sort=NAME, limit=LIST_MAX_ROWS)
202
+ + select_invoice_button())
203
+
204
+ def compact_fetch_invoices_page(search: str) -> str:
205
+ return _selecting_page_(INVOICES_SEARCHING_TITLE,
206
+ vector_select_search(search, SEARCH_INPUTS, SEARCH_RESULTS, model="Invoices")
207
+ + select_invoice_button())
208
+
209
+ def popup_invoices_page(request, search: str) -> str:
210
+ ## <request> is reserved
211
+ if search: return compact_fetch_invoices_page(search)
212
+ return compact_select_invoices_page()
213
+
214
+ def select_payments_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
215
+ where = PAYMENT_NOT_CLOSED if status not in PAYMENT_STATUS_LIST else f"status IN ('{status}')"
216
+ where, sort, asc, limit = adjust_payment_listing_options(where, author, sort, asc, limit, request=request)
217
+ return payments_listing_page(
218
+ title=PAYMENTS_LISTING_TITLE,
219
+ action=f"{FUNCTION_PAYMENTS_SELECT}&status={status}&author={request.session.get(SESSION_USER)}",
220
+ content=list_payments(where=where, sort=sort, asc=asc, limit=limit))
221
+
222
+ def select_payment_page(request, payment_id: str) -> str:
223
+ ## <request> is reserved
224
+ return payment_selecting_page(
225
+ (select_payment(payment_id) or NO_DATA_AVAILABLE)
226
+ + html_link_buttons([call_function(f"{FUNCTION_PAYMENT_UPGRADE}&id={payment_id}")], [LABEL_UPGRADE_PAYMENT_BUTTON], [STYLE_CANCEL_BUTTON]))
227
+
228
+ def update_payment_page(request, payment_id: str, **form) -> str:
229
+ data = get_payment(payment_id)
230
+ if not data: return NO_DATA_AVAILABLE
231
+ if payment_not_updatable(data): return BAD_PAYMENT_ID
232
+ if not len(form):
233
+ return payment_updating_page(
234
+ title=item_content_title(PAYMENT_UPDATING_TITLE, data[NAME]),
235
+ action=f"{FUNCTION_PAYMENT_UPDATE}&id={payment_id}",
236
+ content=dictfilter(data, UPDATE_INPUTS[Payments]))
237
+ update = safe_avro_file_update("Payments", id=payment_id, **form)
238
+ if not update: return FAILED_SAVE_DATA
239
+ _log_update(request, form, "Payments", id=payment_id)
240
+ return CALLBACK
241
+
242
+ def upgrade_payment_page(request, payment_id: str, status: str=None) -> str:
243
+ if not status:
244
+ data = get_payment(payment_id)
245
+ if not data: return NO_DATA_AVAILABLE
246
+ return payment_upgrading_page(
247
+ title=item_content_title(PAYMENT_UPDATING_TITLE, data[NAME]),
248
+ current_status=data[STATUS])
249
+ upgrade = update_close_payment_page(request, payment_id, complete_status=status)
250
+ return upgrade if upgrade not in [DONE_SAVE_DATA] else CALLBACK
251
+
252
+ def update_close_payment_page(request, payment_id: str, complete_status: str=None) -> str:
253
+ data = get_payment(payment_id)
254
+ if not data: return NO_DATA_AVAILABLE
255
+ if payment_closed(data): return PAYMENT_NOT_EXIST_OR_CLOSED
256
+ if complete_status not in PAYMENT_STATUS_LIST:
257
+ status = PAYMENT_CANCELLED
258
+ else:
259
+ status = complete_status
260
+ update = safe_avro_file_update("Payments", id=payment_id, status=status)
261
+ if not update: return FAILED_SAVE_DATA
262
+ insert = safe_avro_file_insert("PaymentsStatus", multi=False, id=payment_id, status=status)
263
+ if not insert: return FAILED_COMMIT_TRANSACTION
264
+ return DONE_SAVE_DATA ## dont edit this return
265
+
266
+ def popup_type_page(request) -> str:
267
+ ## <request> is reserved
268
+ return compact_select_type_page(TYPE_PICKING_TITLE, LABEL_TYPE_PICKING_BUTTON)
269
+
270
+ def compact_select_type_page(title, pick_button, picker=TYPE, pick_data=PAYMENT_TYPE_LIST, opener_getter="") -> str:
271
+ data = escape_punctuation_for_title_list(pick_data)
272
+ ## module <tagrepo.datatemp> must be upgraded to use <schema15> libs
273
+ return _picking_page_(title, data, pick_button, picker=picker, getter=opener_getter)
274
+
275
+ def select_draft_payments_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
276
+ where = PAYMENT_IS_UPDATABLE
277
+ where, sort, asc, limit = adjust_payment_listing_options(where, author, sort, asc, limit, request=request)
278
+ return draft_payments_listing_page(
279
+ title=DRAFT_PAYMENTS_LISTING_TITLE,
280
+ action=f"{FUNCTION_DRAFT_PAYMENTS_SELECT}&author={request.session.get(SESSION_USER)}",
281
+ content=list_payments(where=where, sort=sort, asc=asc, limit=limit))
282
+
283
+ def select_no_draft_payments_page(request, status: str="", sort: str="", asc: str="", limit: str="", author: str="") -> str:
284
+ where = PAYMENT_IS_LISTABLE
285
+ where, sort, asc, limit = adjust_payment_listing_options(where, author, sort, asc, limit, request=request)
286
+ return no_draft_payments_listing_page(
287
+ title=NO_DRAFT_PAYMENTS_LISTING_TITLE,
288
+ action=f"{FUNCTION_NO_DRAFT_PAYMENTS_SELECT}&author={request.session.get(SESSION_USER)}",
289
+ content=list_payments(where=where, sort=sort, asc=asc, limit=limit))
290
+
291
+ def review_payments_page(request) -> str:
292
+ ## <request> is reserved
293
+ return payments_reviewing_page(PAYMENTS_LISTING_TITLE)
294
+
295
+ def _date_range_fieldset(labels: list=[LABEL_TAX_START, LABEL_TAX_END], legend: str=LABEL_TAX_RANGE_INPUT_PARAMS) -> str:
296
+ return html_input_date_range_fieldset(DATED, DATED, labels=labels, legend=legend)
297
+
298
+ def collect_tax_invoices_page(request, dated: str=None, dated_end: str=None) -> str:
299
+ ## <request> is reserved
300
+ if not dated: return tax_invoices_collecting_open_page(_date_range_fieldset())
301
+ return tax_invoices_collecting_page(list_tax_invoices(dated=dated, dated_end=dated_end))
invoiceum/template.py ADDED
@@ -0,0 +1,140 @@
1
+ from tagrepo.datatemp import _creating_page, _selecting_page, _listing_page, _updating_page, _searching_page, _upgrading_page, _reviewing_page, _fetching_page, _picking_page
2
+
3
+ from orderum.template import *
4
+
5
+ from .data import *
6
+ from .title import *
7
+
8
+ ## private functions
9
+
10
+ def _upgrading_page_(title, current_status, status_id="INVOICE_STATUS_LIST") -> str:
11
+ return _upgrading_page(title, current_status, status_id, app_desc=APP_DESC)
12
+
13
+ def _reviewing_page_(title, status_id="INVOICE_STATUS_LIST") -> str:
14
+ return _reviewing_page(title, status_id=status_id, app_desc=APP_DESC)
15
+
16
+ def _fetching_page_(request, search: str, reset: bool=False, title: str=INVOICES_SEARCHING_TITLE, model: str="Invoices", callfunction: str=FUNCTION_INVOICE_SELECT, seek: str="", app_desc: str=APP_DESC, search_inputs: dict=SEARCH_INPUTS, search_results: int=SEARCH_RESULTS, search_option=get_search_author_option) -> str:
17
+ return _fetching_page(request, search, reset=reset, title=title, model=model, callfunction=callfunction, seek=seek, app_desc=app_desc, search_inputs=search_inputs, search_results=search_results, search_option=search_option)
18
+
19
+ def _selecting_page_(title, content) -> str:
20
+ return _selecting_page(title, content, app_desc=APP_DESC)
21
+
22
+ def _listing_page_(title, action, content, submit_button) -> str:
23
+ return _listing_page(title, action, content, submit_button, app_desc=APP_DESC)
24
+
25
+ def _updating_page_(title, action, content: dict) -> str:
26
+ return _updating_page(title, action, content, app_desc=APP_DESC)
27
+
28
+ def _searching_page_(title, search_id="AUTHOR_SEARCH_OPTIONS") -> str:
29
+ return _searching_page(title, search_id, app_desc=APP_DESC)
30
+
31
+ def _picking_page_(title: str, content: list[dict], pick_button: str, picker: str, getter=NAME) -> str:
32
+ return _picking_page(title=title, content=content, pick_button=pick_button, picker=picker, getter=getter, app_desc=APP_DESC)
33
+
34
+ ## public functions
35
+
36
+ def invoice_creating_page() -> str:
37
+ return _creating_page(
38
+ title=INVOICES_CREATING_TITLE,
39
+ action=FUNCTION_INVOICE_INSERT,
40
+ model="Invoices",
41
+ input_label=LABEL_INVOICE_INPUT_PARAMS,
42
+ input_fields=INVOICE_INSERT_FIELDS,
43
+ app_desc=APP_DESC,
44
+ onsubmit=html_form_onsubmit(ARE_YOU_SURE),
45
+ customer_id=html_popup_input_wide_text(CUSTOMER_ID, CUSTOMER_ID, "customer_id", label=LABEL_INVOICE_CUSTOMER_ID, popup_page=FUNCTION_CUSTOMERS_POPUP),
46
+ dated=html_input_date(DATED, DATED, LABEL_INVOICE_DATED),
47
+ due=html_input_date(DUE, DUE, LABEL_INVOICE_DUE),
48
+ note=html_input_textarea(NOTE, NOTE))
49
+
50
+ def invoices_listing_page(title: str, action: str, content: str, label: str=LABEL_SELECT_OWN_INVOICE_BUTTON) -> str:
51
+ return _listing_page_(title, action, content, submit_button=label)
52
+
53
+ draft_invoices_listing_page = invoices_listing_page
54
+ no_draft_invoices_listing_page = invoices_listing_page
55
+
56
+ def invoice_selecting_page(content: str) -> str:
57
+ return _selecting_page_(title=INVOICE_INFO_TITLE, content=content)
58
+
59
+ invoice_updating_page = _updating_page_
60
+
61
+ def invoice_upgrading_page(title: str, current_status: str) -> str:
62
+ return _upgrading_page_(title=title, current_status=current_status, status_id="INVOICE_STATUS_LIST")
63
+
64
+ def invoiceitem_creating_page(title: str, action: str) -> str:
65
+ return _creating_page(
66
+ title=title,
67
+ action=action,
68
+ model="Invoiceitems",
69
+ input_label=LABEL_INVOICEITEM_INPUT_PARAMS,
70
+ input_fields=INVOICEITEM_INSERT_FIELDS,
71
+ app_desc=APP_DESC,
72
+ product_id=html_popup_input_wide_text(PRODUCT_ID, PRODUCT_ID, "product_id", label=LABEL_INVOICEITEM_PRODUCT_ID, popup_page=FUNCTION_PRODUCTS_POPUP),
73
+ note=html_input_textarea(NOTE, NOTE))
74
+
75
+ def invoiceitems_listing_page(title: str, action: str, content: str, label: str=LABEL_CREATE_INVOICEITEM_BUTTON) -> str:
76
+ return _listing_page_(title, action, content, submit_button=label)
77
+
78
+ def invoiceitem_selecting_page(content: str) -> str:
79
+ return _selecting_page_(title=INVOICEITEM_INFO_TITLE, content=content)
80
+
81
+ invoiceitem_updating_page = _updating_page_
82
+
83
+ def invoices_reviewing_page(title: str) -> str:
84
+ return _reviewing_page_(title)
85
+
86
+ ## vector search
87
+
88
+ def invoices_searching_page() -> str:
89
+ return _searching_page_(title=INVOICES_SEARCHING_TITLE, search_id="INVOICE_SEARCH_OPTIONS")
90
+
91
+ def invoices_fetching_page(request, search: str, reset: bool=False, seek: str="", author: str="") -> str:
92
+ return _fetching_page_(request, search, reset=reset, seek=seek, search_option=get_search_invoice_option(request, seek, author=author))
93
+
94
+ def invoices_author_searching_page() -> str:
95
+ return _searching_page_(title=INVOICES_SEARCHING_TITLE)
96
+
97
+ def invoices_author_fetching_page(request, search: str, reset: bool=False, seek: str="") -> str:
98
+ return _fetching_page_(request, search, reset=reset, seek=seek, title=INVOICES_SEARCHING_TITLE, model="Invoices", callfunction=FUNCTION_INVOICE_SELECT)
99
+
100
+ ## invoice payments
101
+
102
+ def payment_creating_page() -> str:
103
+ return _creating_page(
104
+ title=PAYMENTS_CREATING_TITLE,
105
+ action=FUNCTION_PAYMENT_INSERT,
106
+ model="Payments",
107
+ input_label=LABEL_PAYMENT_INPUT_PARAMS,
108
+ input_fields=PAYMENT_INSERT_FIELDS,
109
+ app_desc=APP_DESC,
110
+ onsubmit=html_form_onsubmit(ARE_YOU_SURE),
111
+ invoice_id=html_popup_input_wide_text(INVOICE_ID, INVOICE_ID, "invoice_id", label=LABEL_PAYMENT_INVOICE_ID, popup_page=FUNCTION_INVOICES_POPUP),
112
+ type=html_select_options(TYPE, PAYMENT_TYPES, list_captioning("PAYMENT_TYPES")),
113
+ dated=html_input_date(DATED, DATED, LABEL_PAYMENT_DATED),
114
+ note=html_input_textarea(NOTE, NOTE))
115
+
116
+ def select_invoice_button() -> str:
117
+ return html_pass_buttons([INVOICE_ID], [ID], [LABEL_SELECT_INVOICE_BUTTON], [STYLE_SUBMIT_BUTTON])
118
+
119
+ def payments_listing_page(title: str, action: str, content: str, label: str=LABEL_SELECT_OWN_PAYMENT_BUTTON) -> str:
120
+ return _listing_page_(title, action, content, submit_button=label)
121
+
122
+ def payment_selecting_page(content: str) -> str:
123
+ return _selecting_page_(title=PAYMENT_INFO_TITLE, content=content)
124
+
125
+ payment_updating_page = _updating_page_
126
+
127
+ def payment_upgrading_page(title: str, current_status: str) -> str:
128
+ return _upgrading_page_(title=title, current_status=current_status, status_id="PAYMENT_STATUS_LIST")
129
+
130
+ draft_payments_listing_page = payments_listing_page
131
+ no_draft_payments_listing_page = payments_listing_page
132
+
133
+ def payments_reviewing_page(title: str) -> str:
134
+ return _reviewing_page_(title, status_id="PAYMENT_STATUS_LIST")
135
+
136
+ def tax_invoices_collecting_open_page(content: str) -> str:
137
+ return _listing_page_(INVOICES_TAX_LISTING_TITLE, FUNCTION_TAX_COLLECT, content, LABEL_SUBMIT_BUTTON)
138
+
139
+ def tax_invoices_collecting_page(content: str) -> str:
140
+ return _selecting_page_(title=INVOICES_TAX_TITLE, content=content)
invoiceum/title.py ADDED
@@ -0,0 +1,70 @@
1
+ from orderum.title import *
2
+
3
+ APP_DESC = 'Literp Invoicing Management'
4
+
5
+ set_environ('FORM_TITLE', 'Invoicing Management')
6
+
7
+ set_environ('LABEL_INVOICE_INSERT', 'Add Invoice')
8
+ set_environ('LABEL_INVOICES_SELECT', 'List Invoices')
9
+ set_environ('LABEL_DRAFT_INVOICES_SELECT', 'Fill Invoices')
10
+ set_environ('LABEL_NO_DRAFT_INVOICES_SELECT', 'Browse Invoices')
11
+ set_environ('LABEL_INVOICES_REVIEW', 'Review Invoices')
12
+ set_environ('LABEL_PAYMENT_INSERT', 'Add Payment')
13
+ set_environ('LABEL_PAYMENTS_SELECT', 'List Payments')
14
+ set_environ('LABEL_DRAFT_PAYMENTS_SELECT', 'Approve Payments')
15
+ set_environ('LABEL_NO_DRAFT_PAYMENTS_SELECT', 'Browse Payments')
16
+ set_environ('LABEL_PAYMENTS_REVIEW', 'Review Payments')
17
+ set_environ('LABEL_TAX_COLLECT', 'Taxes Payable')
18
+ set_environ('LABEL_INVOICES_SEARCH', 'Invoices')
19
+
20
+ set_environ('LABEL_UPGRADE_INVOICE_BUTTON', 'Update Invoice Status')
21
+ set_environ('LABEL_SELECT_OWN_INVOICE_BUTTON', 'List My Invoices')
22
+ set_environ('LABEL_CREATE_INVOICEITEM_BUTTON', 'Add Invoice Item')
23
+ set_environ('LABEL_SELECT_INVOICEITEM_BUTTON', 'List Invoice Items')
24
+ set_environ('LABEL_SELECT_INVOICE_BUTTON', 'Pick Selected')
25
+
26
+ set_environ('LABEL_INVOICE_INPUT_PARAMS', 'Invoice metadata')
27
+ set_environ('LABEL_INVOICE_CUSTOMER_ID', 'Customer ID')
28
+ set_environ('LABEL_INVOICE_DATED', 'Issue Date')
29
+ set_environ('LABEL_INVOICE_DUE', 'Due Date')
30
+ set_environ('LABEL_INVOICEITEM_PRODUCT_ID', 'Product ID')
31
+ set_environ('LABEL_INVOICEITEM_INPUT_PARAMS', 'Invoice item info')
32
+
33
+ set_environ('INVOICES_CREATING_TITLE', 'Invoices Creating')
34
+ set_environ('INVOICES_LISTING_TITLE', 'Invoices Listing')
35
+ set_environ('DRAFT_INVOICES_LISTING_TITLE', 'Draft Invoices Listing')
36
+ set_environ('NO_DRAFT_INVOICES_LISTING_TITLE', 'Live Invoices Listing')
37
+
38
+ set_environ('INVOICES_SEARCHING_TITLE', 'Invoices Searching')
39
+ set_environ('INVOICE_INFO_TITLE', 'Invoice Detailed Information')
40
+ set_environ('INVOICE_UPDATING_TITLE', 'Invoice Info Update')
41
+
42
+ set_environ('INVOICEITEMS_CREATING_TITLE', 'Invoice Items Creating')
43
+ set_environ('INVOICEITEMS_LISTING_TITLE', 'Invoice Items Listing')
44
+ set_environ('INVOICEITEM_INFO_TITLE', 'Invoice Item Information')
45
+ set_environ('INVOICEITEM_UPDATING_TITLE', 'Invoice Item Update')
46
+
47
+ set_environ('LABEL_PAYMENT_INPUT_PARAMS', 'Payment metadata')
48
+ set_environ('LABEL_PAYMENT_INVOICE_ID', 'Invoice ID')
49
+ set_environ('LABEL_PAYMENT_DATED', 'Payment Date')
50
+
51
+ set_environ('LABEL_UPGRADE_PAYMENT_BUTTON', 'Update Payment Status')
52
+ set_environ('LABEL_SELECT_OWN_PAYMENT_BUTTON', 'List My Payments')
53
+
54
+ set_environ('PAYMENTS_CREATING_TITLE', 'Payments Creating')
55
+ set_environ('PAYMENTS_LISTING_TITLE', 'Payments Listing')
56
+ set_environ('DRAFT_PAYMENTS_LISTING_TITLE', 'Draft Payments Listing')
57
+ set_environ('NO_DRAFT_PAYMENTS_LISTING_TITLE', 'Live Payments Listing')
58
+
59
+ set_environ('PAYMENT_INFO_TITLE', 'Payment Detailed Information')
60
+ set_environ('PAYMENT_UPDATING_TITLE', 'Payment Info Update')
61
+
62
+ set_environ('TYPE_PICKING_TITLE', 'Payment Type Picking')
63
+ set_environ('LABEL_TYPE_PICKING_BUTTON', 'Pick Selected Payment Type')
64
+
65
+ set_environ('LABEL_TAX_RANGE_INPUT_PARAMS', 'Taxable invoice date range')
66
+ set_environ('LABEL_TAX_START', 'Starting')
67
+ set_environ('LABEL_TAX_END', 'Ending')
68
+
69
+ set_environ('INVOICES_TAX_LISTING_TITLE', 'Taxes Payable Listing')
70
+ set_environ('INVOICES_TAX_TITLE', 'Taxes Payable')
@@ -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,15 @@
1
+ invoiceum/__init__.py,sha256=XZXKnUj-WIqOvYMHiMmi_SSuRP7Ff9v4XgUo81k0EzI,50
2
+ invoiceum/cinvoice.py,sha256=sH72ZSZ9qYIXa-Q0bzB9CDYPqq8MIrYr65F3G2qi85o,12173
3
+ invoiceum/data.py,sha256=Km2dFJOjCrk1D6rRYdU-zlgCW1_MkX3Z2DFNhLICD2o,5493
4
+ invoiceum/environ.py,sha256=ZDAM6lXdI2DYYyUrG-m9W3FHZ2Fn1b5SIHg34eUbjJM,1033
5
+ invoiceum/param.py,sha256=srLjVRdi-9Y4BEbo7xoL-uEC8f6pyfydhXyTQoSgATo,1043
6
+ invoiceum/schema.py,sha256=N-_wNLS9aZ4NxB8UiFqMdP8pUI9p_h09DPvkcEZQTGY,3272
7
+ invoiceum/syslogin.py,sha256=KgTyO9ienMwkRRsC1Zt7X7D5VrlVTPt0Y1x3QzSGOZU,4232
8
+ invoiceum/task.py,sha256=4qv9GMkCvCBElcWMoqDzS5I23qLJBP2_UstED18pAlo,15245
9
+ invoiceum/template.py,sha256=yo1XxRzDJ2rXmflBcXuqTCepFkV-n97K5Zju3RabssA,6775
10
+ invoiceum/title.py,sha256=VUzS8YJsUMpFCMqi-49NHSS5KDYaDF_HF11pumpkf3A,3356
11
+ invoiceum-1.0.0.dist-info/licenses/LICENSE,sha256=4npUbkrpgB6lqMiYYeUxZAP4SOkjVSwK8-7jW60mxvw,1081
12
+ invoiceum-1.0.0.dist-info/METADATA,sha256=-nonKPqvUuIJvSuXx_YaOgWN-56Fi5yFht4m_WVf_0Y,1829
13
+ invoiceum-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
14
+ invoiceum-1.0.0.dist-info/top_level.txt,sha256=v0vVcB4W2i919Js9YPoXrS9XnS3lB1AvHiwklTYgXSA,10
15
+ invoiceum-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ invoiceum