Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions sf_trading/api/customer_override.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Customer overrides: require attachment when VAT Registration Number is set.
"""

from __future__ import annotations

import frappe
from frappe import _


def validate(doc, _method=None):
"""Require at least one attachment when the customer has a VAT Registration Number."""
vat_number = frappe.utils.cstr(doc.get("custom_vat_registration_number") or "").strip()
if not vat_number or vat_number == "0":
return

# Skip on first save — attachment section only appears after the doc exists
if doc.flags.get("in_insert"):
return

attachments = frappe.get_all(
"File",
filters={"attached_to_doctype": "Customer", "attached_to_name": doc.name},
limit=1,
)
if not attachments:
frappe.throw(
_(
"Customer {0} has a VAT Registration Number ({1}). "
"Please attach the required VAT document before saving."
).format(doc.customer_name or doc.name, vat_number)
)
65 changes: 65 additions & 0 deletions sf_trading/api/sales_invoice_override.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
Sales Invoice overrides: remove empty item rows before validation (barcode scanner scan row).
"""

from __future__ import annotations

import frappe
from frappe import _
from frappe.utils import today


def before_validate(doc, _method=None):
"""Remove item rows that have no item_code (leftover scan row from barcode). Runs before validation."""
if not doc.get("items"):
frappe.throw(_("Please add at least one item before saving."))

# Remove in reverse so indices stay valid
to_remove = [row for row in doc.items if not (row.get("item_code") or "").strip()]
for row in to_remove:
doc.remove(row)
for i, row in enumerate(doc.items, start=1):
row.idx = i

if not doc.items:
frappe.throw(_("Please add at least one item before saving."))


def validate(doc, _method=None):
"""Block new credit invoice if customer has overdue unsettled credit (> 30 days)."""
if doc.custom_payment_mode != "Credit":
return
if not doc.customer:
return

overdue = frappe.db.sql(
"""
SELECT name, posting_date, outstanding_amount
FROM `tabSales Invoice`
WHERE customer = %s
AND company = %s
AND custom_payment_mode = 'Credit'
AND docstatus = 1
AND outstanding_amount > 0
AND DATEDIFF(%s, posting_date) > 30
LIMIT 1
""",
(doc.customer, doc.company, today()),
as_dict=True,
)

if overdue:
inv = overdue[0]
frappe.throw(
_(
"Cannot create a new credit invoice for {0}. "
"Invoice {1} dated {2} has an outstanding amount of {3} "
"that is more than 30 days overdue. "
"Please settle the outstanding balance first."
).format(
doc.customer,
inv.name,
inv.posting_date,
frappe.utils.fmt_money(inv.outstanding_amount, currency=doc.currency),
)
)
44 changes: 44 additions & 0 deletions sf_trading/api/sales_invoice_payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,50 @@

import frappe
from frappe import _
from frappe.utils import flt


@frappe.whitelist()
def get_available_credit(customer: str, company: str) -> float:
"""
Return the available credit for a customer:
credit_limit (from Customer Credit Limit child table)
minus sum of outstanding_amount on submitted Sales Invoices
where custom_payment_mode = 'Credit'.
"""
if not customer or not company:
return 0.0

# Credit limit defined on Customer for this company
row = frappe.db.get_value(
"Customer Credit Limit",
{"parent": customer, "company": company},
"credit_limit",
)
if row is None:
# Fallback: first credit limit regardless of company
row = frappe.db.get_value(
"Customer Credit Limit",
{"parent": customer},
"credit_limit",
)
credit_limit = flt(row)
if not credit_limit:
return 0.0

used = frappe.db.sql(
"""
SELECT IFNULL(SUM(grand_total), 0)
FROM `tabSales Invoice`
WHERE customer = %s
AND company = %s
AND custom_payment_mode = 'Credit'
AND docstatus = 1
""",
(customer, company),
)[0][0]

return max(0.0, flt(credit_limit) - flt(used))


@frappe.whitelist()
Expand Down
Loading
Loading