Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.
This commit is contained in:
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user