mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-24 22:07:05 +00:00
Compare commits
10 Commits
develop
...
proforma-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab34dc9025 | ||
|
|
e19c3c6984 | ||
|
|
defff46d8f | ||
|
|
7dd3598156 | ||
|
|
6bf5fa51d5 | ||
|
|
d29ccfb569 | ||
|
|
c1808c9124 | ||
|
|
fa9d7a75e1 | ||
|
|
02854a48ee | ||
|
|
b03e48f453 |
@@ -458,6 +458,12 @@ def validate_child_on_delete(row, parent, ordered_item=None) -> None:
|
||||
"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
|
||||
).format(row.idx, row.item_code)
|
||||
)
|
||||
if frappe.db.exists("Proforma Invoice Item", {"so_detail": row.name, "docstatus": 1}):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has an issued Proforma Invoice.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype == "Purchase Order" and flt(row.received_qty):
|
||||
frappe.throw(
|
||||
|
||||
@@ -4,19 +4,21 @@
|
||||
frappe.ui.form.on("Sales Order", {
|
||||
refresh(frm) {
|
||||
erpnext.proforma.toggle_tab(frm, false);
|
||||
if (frm.doc.docstatus !== 1) return;
|
||||
if (frm.doc.docstatus === 0) return;
|
||||
|
||||
frappe.db.get_single_value("Selling Settings", "enable_proforma_invoice").then((enabled) => {
|
||||
if (!enabled) return;
|
||||
|
||||
// Defer so the button lands after the standard Create options, not before them.
|
||||
setTimeout(() => {
|
||||
frm.add_custom_button(
|
||||
__("Proforma Invoice"),
|
||||
() => erpnext.proforma.open_dialog(frm),
|
||||
__("Create")
|
||||
);
|
||||
}, 0);
|
||||
if (frm.doc.docstatus === 1) {
|
||||
// Defer so the button lands after the standard Create options, not before them.
|
||||
setTimeout(() => {
|
||||
frm.add_custom_button(
|
||||
__("Proforma Invoice"),
|
||||
() => erpnext.proforma.open_dialog(frm),
|
||||
__("Create")
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
erpnext.proforma.render_list(frm);
|
||||
});
|
||||
},
|
||||
@@ -117,6 +119,12 @@ Object.assign(erpnext.proforma, {
|
||||
read_only: 1,
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "description",
|
||||
fieldtype: "Text Editor",
|
||||
label: __("Description"),
|
||||
in_list_view: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "qty",
|
||||
fieldtype: "Float",
|
||||
@@ -205,11 +213,12 @@ Object.assign(erpnext.proforma, {
|
||||
const by_amount = values.based_on === "Amount";
|
||||
const items = (values.items || [])
|
||||
.filter((row) => flt(by_amount ? row.amount : row.qty) > 0)
|
||||
.map((row) =>
|
||||
by_amount
|
||||
? { so_detail: row.so_detail, qty: row.qty, amount: row.amount }
|
||||
: { so_detail: row.so_detail, qty: row.qty }
|
||||
);
|
||||
.map((row) => ({
|
||||
so_detail: row.so_detail,
|
||||
description: row.description,
|
||||
qty: row.qty,
|
||||
amount: row.amount,
|
||||
}));
|
||||
|
||||
if (!items.length) {
|
||||
frappe.msgprint(__("Please enter a quantity or amount for at least one item."));
|
||||
@@ -314,6 +323,7 @@ Object.assign(erpnext.proforma, {
|
||||
],
|
||||
});
|
||||
list.refresh();
|
||||
if (frm.doc.docstatus !== 1) return;
|
||||
|
||||
frappe.ui
|
||||
.button({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Proforma Invoice", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
frappe.ui.form.on("Proforma Invoice", {
|
||||
refresh(frm) {
|
||||
frm.page.btn_primary.toggle(frm.doc.docstatus !== 2);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,8 +45,59 @@ class ProformaInvoice(Document):
|
||||
|
||||
def validate(self) -> None:
|
||||
validate_feature_enabled()
|
||||
self.validate_amended_doc()
|
||||
self.validate_sales_order()
|
||||
self.set_item_values()
|
||||
self.set_total_qty()
|
||||
|
||||
def validate_sales_order(self) -> None:
|
||||
if frappe.db.get_value("Sales Order", self.sales_order, "docstatus") != 1:
|
||||
frappe.throw(_("A Proforma Invoice can only be created against a submitted Sales Order."))
|
||||
|
||||
def set_item_values(self) -> None:
|
||||
"""Copy each line's item details from its Sales Order line, then set the rate and amount."""
|
||||
so_items = {
|
||||
row.name: row
|
||||
for row in frappe.get_all(
|
||||
"Sales Order Item",
|
||||
filters={"parent": self.sales_order, "parenttype": "Sales Order"},
|
||||
fields=["name", "item_code", "item_name", "description", "uom", "rate"],
|
||||
)
|
||||
}
|
||||
for item in self.items:
|
||||
so_item = so_items.get(item.so_detail)
|
||||
if not so_item:
|
||||
frappe.throw(
|
||||
_("Row #{0}: The line does not belong to Sales Order {1}").format(
|
||||
item.idx, frappe.bold(self.sales_order)
|
||||
)
|
||||
)
|
||||
item.item_code = so_item.item_code
|
||||
item.item_name = so_item.item_name
|
||||
item.uom = so_item.uom
|
||||
item.description = item.description or so_item.description
|
||||
self.set_rate_and_amount(item, so_item.rate)
|
||||
|
||||
def set_rate_and_amount(self, item, sales_order_rate: float) -> None:
|
||||
"""Quantity basis bills at the Sales Order rate; Amount basis derives the rate from the amount."""
|
||||
if flt(item.qty) <= 0:
|
||||
frappe.throw(_("Row #{0}: Qty must be a positive number").format(item.idx))
|
||||
if self.based_on == "Amount":
|
||||
if flt(item.amount) <= 0:
|
||||
frappe.throw(_("Row #{0}: Amount must be a positive number").format(item.idx))
|
||||
item.rate = flt(item.amount) / flt(item.qty)
|
||||
else:
|
||||
item.rate = sales_order_rate
|
||||
item.amount = flt(item.qty) * flt(sales_order_rate)
|
||||
|
||||
def validate_amended_doc(self) -> None:
|
||||
if self.amended_from:
|
||||
frappe.throw(
|
||||
_("Cannot amend {0} {1}, please create a new one instead.").format(
|
||||
self.doctype, frappe.bold(self.amended_from)
|
||||
)
|
||||
)
|
||||
|
||||
def before_submit(self) -> None:
|
||||
self.status = "Issued"
|
||||
|
||||
@@ -80,6 +131,7 @@ class ProformaInvoice(Document):
|
||||
for item in sales_order.items:
|
||||
item.qty = lines[item.name].qty
|
||||
item.rate = lines[item.name].rate
|
||||
item.description = lines[item.name].description
|
||||
item.discount_amount = 0
|
||||
item.discount_percentage = 0
|
||||
sales_order.run_method("calculate_taxes_and_totals")
|
||||
@@ -116,6 +168,7 @@ def get_sales_order_items(sales_order: str) -> list[dict]:
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"uom": item.uom,
|
||||
"so_detail": item.name,
|
||||
"qty": flt(item.qty),
|
||||
@@ -155,19 +208,13 @@ def make_proforma_invoice(
|
||||
print_format: str | None = None,
|
||||
letter_head: str | None = None,
|
||||
) -> str:
|
||||
"""The sole creation path for a Proforma Invoice (the doctype is `in_create`).
|
||||
"""Create and submit a Proforma Invoice from the Sales Order dialog.
|
||||
|
||||
`based_on` decides what the user edited per line: "Quantity" (rate fixed, amount = qty x rate)
|
||||
or "Amount" (both qty and amount entered, rate derived). `hide_item_qty` (Amount basis only)
|
||||
hides the qty and rate on the printed proforma for a clean value-based document.
|
||||
"""
|
||||
validate_feature_enabled()
|
||||
selected = frappe.parse_json(items)
|
||||
sales_order_doc = frappe.get_doc("Sales Order", sales_order)
|
||||
if sales_order_doc.docstatus != 1:
|
||||
frappe.throw(_("A Proforma Invoice can only be created against a submitted Sales Order."))
|
||||
so_items = {item.name: item for item in sales_order_doc.items}
|
||||
|
||||
proforma = frappe.new_doc("Proforma Invoice")
|
||||
proforma.sales_order = sales_order
|
||||
proforma.based_on = based_on
|
||||
@@ -179,13 +226,16 @@ def make_proforma_invoice(
|
||||
)
|
||||
proforma.letter_head = letter_head
|
||||
|
||||
for row in selected:
|
||||
so_item = so_items.get(row.get("so_detail"))
|
||||
if not so_item:
|
||||
continue
|
||||
line = _proforma_line(so_item, based_on, row)
|
||||
if line:
|
||||
proforma.append("items", line)
|
||||
for row in frappe.parse_json(items):
|
||||
proforma.append(
|
||||
"items",
|
||||
{
|
||||
"so_detail": row.get("so_detail"),
|
||||
"qty": row.get("qty"),
|
||||
"amount": row.get("amount"),
|
||||
"description": row.get("description"),
|
||||
},
|
||||
)
|
||||
|
||||
if not proforma.items:
|
||||
frappe.throw(_("Please enter a quantity or amount for at least one item."))
|
||||
@@ -195,32 +245,6 @@ def make_proforma_invoice(
|
||||
return proforma.name
|
||||
|
||||
|
||||
def _proforma_line(so_item, based_on: str, row: dict) -> dict | None:
|
||||
if based_on == "Amount":
|
||||
# Amount basis: both qty and amount are user-entered; the rate is derived.
|
||||
qty = flt(row.get("qty"))
|
||||
amount = flt(row.get("amount"))
|
||||
if amount <= 0 or qty <= 0:
|
||||
return None
|
||||
rate = amount / qty
|
||||
else:
|
||||
qty = flt(row.get("qty"))
|
||||
if qty <= 0:
|
||||
return None
|
||||
rate = flt(so_item.rate)
|
||||
amount = qty * rate
|
||||
|
||||
return {
|
||||
"item_code": so_item.item_code,
|
||||
"item_name": so_item.item_name,
|
||||
"uom": so_item.uom,
|
||||
"qty": qty,
|
||||
"rate": rate,
|
||||
"amount": amount,
|
||||
"so_detail": so_item.name,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_proforma_email(proforma_name: str, recipients: str) -> None:
|
||||
proforma = frappe.get_doc("Proforma Invoice", proforma_name)
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.services.child_item_update import update_child_qty_rate
|
||||
from erpnext.selling.doctype.proforma_invoice.proforma_invoice import (
|
||||
get_sales_order_items,
|
||||
make_proforma_invoice,
|
||||
@@ -24,6 +25,9 @@ class TestProformaInvoice(ERPNextTestSuite):
|
||||
name = make_proforma_invoice(sales_order.name, json.dumps(items), **kwargs)
|
||||
return frappe.get_doc("Proforma Invoice", name)
|
||||
|
||||
def make_draft_proforma(self, sales_order, **item):
|
||||
return frappe.new_doc("Proforma Invoice", sales_order=sales_order.name, items=[item]).insert()
|
||||
|
||||
def test_partial_proforma_is_non_blocking(self):
|
||||
"""A proforma must not touch delivery/billing or the source Sales Order."""
|
||||
sales_order = make_sales_order(qty=10)
|
||||
@@ -172,6 +176,74 @@ class TestProformaInvoice(ERPNextTestSuite):
|
||||
("Proforma Invoice PRO-TEST-0001", "Please find attached the proforma invoice PRO-TEST-0001."),
|
||||
)
|
||||
|
||||
def test_line_description_is_editable(self):
|
||||
sales_order = make_sales_order(qty=10, do_not_submit=True)
|
||||
sales_order.items[0].description = "Ordered description"
|
||||
sales_order.submit()
|
||||
so_detail = sales_order.items[0].name
|
||||
|
||||
edited = make_proforma_invoice(
|
||||
sales_order.name, json.dumps([{"so_detail": so_detail, "qty": 4, "description": "Edited"}])
|
||||
)
|
||||
unedited = self.create_proforma(sales_order, [(so_detail, 4)])
|
||||
|
||||
self.assertEqual(get_sales_order_items(sales_order.name)[0]["description"], "Ordered description")
|
||||
self.assertEqual(frappe.get_doc("Proforma Invoice", edited).items[0].description, "Edited")
|
||||
self.assertEqual(unedited.items[0].description, "Ordered description")
|
||||
|
||||
def test_update_items_cannot_delete_a_proformed_row(self):
|
||||
sales_order = make_sales_order(
|
||||
item_list=[
|
||||
{"item_code": "_Test Item", "qty": 5, "rate": 100},
|
||||
{"item_code": "_Test Item 2", "qty": 2, "rate": 50},
|
||||
]
|
||||
)
|
||||
proformed, other = sales_order.items
|
||||
proforma = self.create_proforma(sales_order, [(proformed.name, 2)])
|
||||
keep_other = json.dumps(
|
||||
[{"item_code": other.item_code, "qty": other.qty, "rate": other.rate, "docname": other.name}]
|
||||
)
|
||||
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_child_qty_rate, "Sales Order", keep_other, sales_order.name
|
||||
)
|
||||
|
||||
proforma.cancel()
|
||||
update_child_qty_rate("Sales Order", keep_other, sales_order.name)
|
||||
sales_order.reload()
|
||||
self.assertEqual([item.name for item in sales_order.items], [other.name])
|
||||
|
||||
def test_line_from_another_sales_order_is_rejected(self):
|
||||
sales_order = make_sales_order(qty=10)
|
||||
other_item = make_sales_order(qty=10).items[0]
|
||||
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
self.make_draft_proforma,
|
||||
sales_order,
|
||||
so_detail=other_item.name,
|
||||
item_code=other_item.item_code,
|
||||
qty=4,
|
||||
)
|
||||
|
||||
def test_quantity_basis_bills_at_sales_order_rate(self):
|
||||
sales_order = make_sales_order(qty=10)
|
||||
so_item = sales_order.items[0]
|
||||
|
||||
proforma = self.make_draft_proforma(
|
||||
sales_order, so_detail=so_item.name, item_code=so_item.item_code, qty=4, rate=1, amount=1
|
||||
)
|
||||
|
||||
item = proforma.items[0]
|
||||
self.assertEqual(item.item_code, so_item.item_code)
|
||||
self.assertEqual(flt(item.rate), flt(so_item.rate))
|
||||
self.assertEqual(flt(item.amount), 4 * flt(so_item.rate))
|
||||
|
||||
def test_amended_proforma_is_rejected(self):
|
||||
proforma = frappe.get_doc({"doctype": "Proforma Invoice", "amended_from": "PRO-TEST-0001"})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, proforma.validate_amended_doc)
|
||||
|
||||
def test_requires_submitted_sales_order(self):
|
||||
"""The server rejects a proforma against a draft Sales Order (the button is JS-gated only)."""
|
||||
sales_order = make_sales_order(qty=10, do_not_submit=True)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"column_break_qty",
|
||||
"qty",
|
||||
"uom",
|
||||
@@ -32,6 +33,11 @@
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qty",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -79,7 +85,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-16 00:00:00.000000",
|
||||
"modified": "2026-09-24 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Proforma Invoice Item",
|
||||
|
||||
@@ -14,6 +14,7 @@ class ProformaInvoiceItem(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
description: DF.TextEditor | None
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
parent: DF.Data
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font_size": 0,
|
||||
"html": "<div class=\"proforma-print\">\n\t<style>\n\t\t.proforma-print { font-family: \"Inter\", sans-serif; color: #1f272e; font-size: 12px; }\n\t\t.proforma-print h2 { margin: 0; font-size: 20px; letter-spacing: 1px; }\n\t\t.proforma-print .muted { color: #6b7280; }\n\t\t.proforma-print table { width: 100%; border-collapse: collapse; }\n\t\t.proforma-print .meta-table td { padding: 2px 0; vertical-align: top; }\n\t\t.proforma-print .items-table th, .proforma-print .items-table td {\n\t\t\tborder-bottom: 1px solid #e5e7eb; padding: 8px 6px; text-align: left;\n\t\t}\n\t\t.proforma-print .items-table th { border-bottom: 2px solid #9ca3af; }\n\t\t.proforma-print .text-right { text-align: right !important; }\n\t\t.proforma-print .totals { width: 45%; margin-left: auto; margin-top: 12px; }\n\t\t.proforma-print .totals td { padding: 4px 6px; }\n\t\t.proforma-print .grand { border-top: 2px solid #9ca3af; font-weight: 600; font-size: 14px; }\n\t\t.proforma-print .footer-note { margin-top: 30px; font-size: 11px; color: #6b7280; }\n\t</style>\n\n\t<table class=\"meta-table\">\n\t\t<tr>\n\t\t\t<td style=\"width: 60%;\">\n\t\t\t\t<h2>{{ _(\"PROFORMA INVOICE\") }}</h2>\n\t\t\t\t<div class=\"muted\">{{ doc.company }}</div>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t<table style=\"width: 100%;\">\n\t\t\t\t\t<tr><td class=\"text-right muted\">{{ _(\"Proforma No\") }}</td><td class=\"text-right\">{{ doc.proforma_no or doc.name }}</td></tr>\n\t\t\t\t\t<tr><td class=\"text-right muted\">{{ _(\"Date\") }}</td><td class=\"text-right\">{{ frappe.utils.formatdate(doc.proforma_date) }}</td></tr>\n\t\t\t\t\t<tr><td class=\"text-right muted\">{{ _(\"Against Sales Order\") }}</td><td class=\"text-right\">{{ doc.name }}</td></tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n\n\t<hr style=\"border: none; border-top: 1px solid #e5e7eb; margin: 14px 0;\">\n\n\t<table class=\"meta-table\">\n\t\t<tr>\n\t\t\t<td><strong>{{ _(\"Bill To\") }}</strong><br>{{ doc.customer_name }}</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{% if doc.customer_address %}{{ doc.get_formatted(\"address_display\") }}{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n\n\t<table class=\"items-table\" style=\"margin-top: 16px;\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th style=\"width: 5%;\">{{ _(\"Sr\") }}</th>\n\t\t\t\t<th style=\"width: 45%;\">{{ _(\"Item\") }}</th>\n\t\t\t\t{% if not doc.hide_item_qty %}<th class=\"text-right\" style=\"width: 14%;\">{{ _(\"Qty\") }}</th>{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}<th class=\"text-right\" style=\"width: 16%;\">{{ _(\"Rate\") }}</th>{% endif %}\n\t\t\t\t<th class=\"text-right\" style=\"width: 20%;\">{{ _(\"Amount\") }}</th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t{% for row in doc.items %}\n\t\t\t<tr>\n\t\t\t\t<td>{{ loop.index }}</td>\n\t\t\t\t<td><strong>{{ row.item_code }}</strong>{% if row.item_name != row.item_code %}<br><span class=\"muted\">{{ row.item_name }}</span>{% endif %}</td>\n\t\t\t\t{% if not doc.hide_item_qty %}<td class=\"text-right\">{{ row.get_formatted(\"qty\") }} {{ row.uom }}</td>{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}<td class=\"text-right\">{{ row.get_formatted(\"rate\", doc) }}</td>{% endif %}\n\t\t\t\t<td class=\"text-right\">{{ row.get_formatted(\"amount\", doc) }}</td>\n\t\t\t</tr>\n\t\t\t{% endfor %}\n\t\t</tbody>\n\t</table>\n\n\t<table class=\"totals\">\n\t\t<tr>\n\t\t\t<td class=\"muted\">{{ _(\"Net Total\") }}</td>\n\t\t\t<td class=\"text-right\">{{ doc.get_formatted(\"net_total\") }}</td>\n\t\t</tr>\n\t\t{% for tax in doc.taxes %}\n\t\t\t{% if tax.tax_amount %}\n\t\t\t<tr>\n\t\t\t\t<td class=\"muted\">{{ tax.description }}</td>\n\t\t\t\t<td class=\"text-right\">{{ tax.get_formatted(\"tax_amount\", doc) }}</td>\n\t\t\t</tr>\n\t\t\t{% endif %}\n\t\t{% endfor %}\n\t\t<tr class=\"grand\">\n\t\t\t<td>{{ _(\"Grand Total\") }}</td>\n\t\t\t<td class=\"text-right\">{{ doc.get_formatted(\"grand_total\") }}</td>\n\t\t</tr>\n\t</table>\n\n\t<div class=\"footer-note\">\n\t\t{{ _(\"This is a proforma invoice and is not a demand for payment or a tax invoice.\") }}\n\t</div>\n</div>\n",
|
||||
"html": "<div class=\"proforma-print\">\n\t<style>\n\t\t.proforma-print { font-family: \"Inter\", sans-serif; color: #1f272e; font-size: 12px; }\n\t\t.proforma-print h2 { margin: 0; font-size: 20px; letter-spacing: 1px; }\n\t\t.proforma-print .muted { color: #6b7280; }\n\t\t.proforma-print table { width: 100%; border-collapse: collapse; }\n\t\t.proforma-print .meta-table td { padding: 2px 0; vertical-align: top; }\n\t\t.proforma-print .items-table th, .proforma-print .items-table td {\n\t\t\tborder-bottom: 1px solid #e5e7eb; padding: 8px 6px; text-align: left;\n\t\t}\n\t\t.proforma-print .items-table th { border-bottom: 2px solid #9ca3af; }\n\t\t.proforma-print .text-right { text-align: right !important; }\n\t\t.proforma-print .totals { width: 45%; margin-left: auto; margin-top: 12px; }\n\t\t.proforma-print .totals td { padding: 4px 6px; }\n\t\t.proforma-print .grand { border-top: 2px solid #9ca3af; font-weight: 600; font-size: 14px; }\n\t</style>\n\n\t<table class=\"meta-table\">\n\t\t<tr>\n\t\t\t<td style=\"width: 60%;\">\n\t\t\t\t<h2>{{ _(\"PROFORMA INVOICE\") }}</h2>\n\t\t\t\t<div class=\"muted\">{{ doc.company }}</div>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t<table style=\"width: 100%;\">\n\t\t\t\t\t<tr><td class=\"text-right muted\">{{ _(\"Proforma No\") }}</td><td class=\"text-right\">{{ doc.proforma_no or doc.name }}</td></tr>\n\t\t\t\t\t<tr><td class=\"text-right muted\">{{ _(\"Date\") }}</td><td class=\"text-right\">{{ frappe.utils.formatdate(doc.proforma_date) }}</td></tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n\n\t<hr style=\"border: none; border-top: 1px solid #e5e7eb; margin: 14px 0;\">\n\n\t<table class=\"meta-table\">\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t<strong>{{ _(\"Bill To\") }}</strong><br>{{ doc.customer_name }}\n\t\t\t\t{% if doc.customer_address %}<br>{{ doc.get_formatted(\"address_display\") }}{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n\n\t<table class=\"items-table\" style=\"margin-top: 16px;\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th style=\"width: 5%;\">{{ _(\"Sr\") }}</th>\n\t\t\t\t<th style=\"width: 45%;\">{{ _(\"Item\") }}</th>\n\t\t\t\t{% if not doc.hide_item_qty %}<th class=\"text-right\" style=\"width: 14%;\">{{ _(\"Qty\") }}</th>{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}<th class=\"text-right\" style=\"width: 16%;\">{{ _(\"Rate\") }}</th>{% endif %}\n\t\t\t\t<th class=\"text-right\" style=\"width: 20%;\">{{ _(\"Amount\") }}</th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t{% for row in doc.items %}\n\t\t\t<tr>\n\t\t\t\t<td>{{ loop.index }}</td>\n\t\t\t\t<td>\n\t\t\t\t\t<strong>{{ row.item_code }}</strong>{% if row.item_name != row.item_code %}<br><span class=\"muted\">{{ row.item_name }}</span>{% endif %}\n\t\t\t\t\t{% if row.description and frappe.utils.strip_html(row.description).strip() != row.item_name %}<div class=\"muted\">{{ row.description }}</div>{% endif %}\n\t\t\t\t</td>\n\t\t\t\t{% if not doc.hide_item_qty %}<td class=\"text-right\">{{ row.get_formatted(\"qty\") }} {{ row.uom }}</td>{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}<td class=\"text-right\">{{ row.get_formatted(\"rate\", doc) }}</td>{% endif %}\n\t\t\t\t<td class=\"text-right\">{{ row.get_formatted(\"amount\", doc) }}</td>\n\t\t\t</tr>\n\t\t\t{% endfor %}\n\t\t</tbody>\n\t</table>\n\n\t<table class=\"totals\">\n\t\t<tr>\n\t\t\t<td class=\"muted\">{{ _(\"Net Total\") }}</td>\n\t\t\t<td class=\"text-right\">{{ doc.get_formatted(\"net_total\") }}</td>\n\t\t</tr>\n\t\t{% for tax in doc.taxes %}\n\t\t\t{% if tax.tax_amount %}\n\t\t\t<tr>\n\t\t\t\t<td class=\"muted\">{{ tax.description }}</td>\n\t\t\t\t<td class=\"text-right\">{{ tax.get_formatted(\"tax_amount\", doc) }}</td>\n\t\t\t</tr>\n\t\t\t{% endif %}\n\t\t{% endfor %}\n\t\t<tr class=\"grand\">\n\t\t\t<td>{{ _(\"Grand Total\") }}</td>\n\t\t\t<td class=\"text-right\">{{ doc.get_formatted(\"grand_total\") }}</td>\n\t\t</tr>\n\t</table>\n</div>\n",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 15.0,
|
||||
"margin_left": 15.0,
|
||||
"margin_right": 15.0,
|
||||
"margin_top": 15.0,
|
||||
"modified": "2026-07-16 00:00:00.000000",
|
||||
"modified": "2026-09-24 12:30:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Proforma Invoice",
|
||||
|
||||
Reference in New Issue
Block a user