Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.

This commit is contained in:
2026-04-17 14:37:36 +03:00
parent eb773c5531
commit 0a8a42d69b
447 changed files with 70696 additions and 492 deletions

View File

@@ -42,6 +42,7 @@ def _float(d: Decimal) -> float:
def _calculate_totals(
items: list,
global_discount_percent: float,
global_vat_percent: float,
shipping_cost: float,
shipping_cost_discount: float,
install_cost: float,
@@ -50,21 +51,20 @@ def _calculate_totals(
) -> dict:
"""
Calculate all monetary totals using Decimal arithmetic (ROUND_HALF_UP).
VAT is computed per-item from each item's vat_percent field.
VAT is a single global rate applied to items only (not shipping or install).
Shipping and install costs carry 0% VAT.
Returns a dict of floats ready for DB storage.
"""
# Per-line totals and per-item VAT
# Per-line totals (items only)
item_totals = []
item_vat = Decimal(0)
for item in items:
cost = _d(item.get("unit_cost", 0))
qty = _d(item.get("quantity", 1))
disc = _d(item.get("discount_percent", 0))
net = cost * qty * (1 - disc / 100)
item_totals.append(net)
vat_pct = _d(item.get("vat_percent", 24))
item_vat += net * (vat_pct / 100)
items_net = sum(item_totals, Decimal(0))
# Shipping net (VAT = 0%)
ship_gross = _d(shipping_cost)
@@ -76,16 +76,17 @@ def _calculate_totals(
install_disc = _d(install_cost_discount)
install_net = install_gross * (1 - install_disc / 100)
subtotal = sum(item_totals, Decimal(0)) + ship_net + install_net
subtotal = items_net + ship_net + install_net
global_disc_pct = _d(global_discount_percent)
global_disc_amount = subtotal * (global_disc_pct / 100)
new_subtotal = subtotal - global_disc_amount
# Global discount proportionally reduces VAT too
if subtotal > 0:
disc_ratio = new_subtotal / subtotal
vat_amount = item_vat * disc_ratio
# VAT applies only to items portion, scaled by the global discount ratio
vat_pct = _d(global_vat_percent)
if subtotal > 0 and items_net > 0:
items_ratio = items_net / subtotal
vat_amount = new_subtotal * items_ratio * (vat_pct / 100)
else:
vat_amount = Decimal(0)
@@ -109,14 +110,16 @@ def _calc_line_total(item) -> float:
async def _generate_quotation_number(db) -> str:
year = datetime.utcnow().year
prefix = f"QT-{year}-"
now = datetime.utcnow()
yy = now.strftime("%y")
mm = now.strftime("%m")
prefix = f"QT-{yy}-{mm}-"
rows = await db.execute_fetchall(
"SELECT quotation_number FROM crm_quotations WHERE quotation_number LIKE ? ORDER BY quotation_number DESC LIMIT 1",
(f"{prefix}%",),
)
if rows:
last_num = rows[0][0] # e.g. "QT-2026-012"
last_num = rows[0][0] # e.g. "QT-26-04-012"
try:
seq = int(last_num[len(prefix):]) + 1
except ValueError:
@@ -174,13 +177,16 @@ async def list_all_quotations() -> list[dict]:
doc = fstore.collection("crm_customers").document(cid).get()
if doc.exists:
d = doc.to_dict()
parts = [d.get("name", ""), d.get("surname", ""), d.get("organization", "")]
label = " ".join(p for p in parts if p).strip()
customer_names[cid] = label or cid
name_parts = [d.get("name", ""), d.get("surname", "")]
full_name = " ".join(p for p in name_parts if p).strip()
org = (d.get("organization", "") or "").strip()
customer_names[cid] = {"name": full_name or cid, "org": org}
except Exception:
customer_names[cid] = cid
customer_names[cid] = {"name": cid, "org": ""}
for item in items:
item["customer_name"] = customer_names.get(item["customer_id"], "")
info = customer_names.get(item["customer_id"], {"name": "", "org": ""})
item["customer_name"] = info["name"]
item["customer_org"] = info["org"]
return items
@@ -222,6 +228,7 @@ async def create_quotation(data: QuotationCreate, generate_pdf: bool = False) ->
totals = _calculate_totals(
items_raw,
data.global_discount_percent,
data.global_vat_percent,
data.shipping_cost,
data.shipping_cost_discount,
data.install_cost,
@@ -236,7 +243,7 @@ async def create_quotation(data: QuotationCreate, generate_pdf: bool = False) ->
"""INSERT INTO crm_quotations (
id, quotation_number, title, subtitle, customer_id,
language, status, order_type, shipping_method, estimated_shipping_date,
global_discount_label, global_discount_percent,
global_discount_label, global_discount_percent, global_vat_percent,
shipping_cost, shipping_cost_discount, install_cost, install_cost_discount,
extras_label, extras_cost, comments, quick_notes,
subtotal_before_discount, global_discount_amount, new_subtotal, vat_amount, final_total,
@@ -247,7 +254,7 @@ async def create_quotation(data: QuotationCreate, generate_pdf: bool = False) ->
) VALUES (
?, ?, ?, ?, ?,
?, 'draft', ?, ?, ?,
?, ?,
?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?,
@@ -259,7 +266,7 @@ async def create_quotation(data: QuotationCreate, generate_pdf: bool = False) ->
(
qid, quotation_number, data.title, data.subtitle, data.customer_id,
data.language, data.order_type, data.shipping_method, data.estimated_shipping_date,
data.global_discount_label, data.global_discount_percent,
data.global_discount_label, data.global_discount_percent, data.global_vat_percent,
data.shipping_cost, data.shipping_cost_discount, data.install_cost, data.install_cost_discount,
data.extras_label, data.extras_cost, comments_json, quick_notes_json,
totals["subtotal_before_discount"], totals["global_discount_amount"],
@@ -317,7 +324,7 @@ async def update_quotation(quotation_id: str, data: QuotationUpdate, generate_pd
scalar_fields = [
"title", "subtitle", "language", "status", "order_type", "shipping_method",
"estimated_shipping_date", "global_discount_label", "global_discount_percent",
"estimated_shipping_date", "global_discount_label", "global_discount_percent", "global_vat_percent",
"shipping_cost", "shipping_cost_discount", "install_cost",
"install_cost_discount", "extras_label", "extras_cost",
"client_org", "client_name", "client_location", "client_phone", "client_email",
@@ -352,6 +359,7 @@ async def update_quotation(quotation_id: str, data: QuotationUpdate, generate_pd
totals = _calculate_totals(
items_raw,
float(merged.get("global_discount_percent", 0)),
float(merged.get("global_vat_percent", 24)),
float(merged.get("shipping_cost", 0)),
float(merged.get("shipping_cost_discount", 0)),
float(merged.get("install_cost", 0)),