mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 07:28:39 +00:00
feat(selling): option to hide item qty on amount-based proforma print
Add a "Hide Item Quantity in Print" option (Amount basis only) that omits the qty and rate columns from the printed proforma, for a clean value-based document that shows only item and amount.
This commit is contained in:
@@ -93,6 +93,12 @@ Object.assign(erpnext.proforma, {
|
||||
default: "Quantity",
|
||||
onchange: () => this.toggle_basis(dialog),
|
||||
},
|
||||
{
|
||||
fieldname: "hide_item_qty",
|
||||
fieldtype: "Check",
|
||||
label: __("Hide Item Quantity in Print"),
|
||||
depends_on: 'eval:doc.based_on=="Amount"',
|
||||
},
|
||||
{
|
||||
fieldname: "items",
|
||||
fieldtype: "Table",
|
||||
@@ -212,6 +218,7 @@ Object.assign(erpnext.proforma, {
|
||||
sales_order: frm.doc.name,
|
||||
items: JSON.stringify(items),
|
||||
based_on: values.based_on,
|
||||
hide_item_qty: values.hide_item_qty ? 1 : 0,
|
||||
naming_series: values.naming_series,
|
||||
print_format: values.print_format,
|
||||
letter_head: values.letter_head,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"company",
|
||||
"currency",
|
||||
"based_on",
|
||||
"hide_item_qty",
|
||||
"items_section",
|
||||
"items",
|
||||
"total_qty",
|
||||
@@ -105,6 +106,15 @@
|
||||
"options": "Quantity\nAmount",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.based_on==\"Amount\"",
|
||||
"description": "Hide the item quantity and rate on the printed proforma.",
|
||||
"fieldname": "hide_item_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Item Quantity in Print",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
|
||||
@@ -30,6 +30,7 @@ class ProformaInvoice(Document):
|
||||
customer_name: DF.Data | None
|
||||
emailed_to: DF.SmallText | None
|
||||
grand_total: DF.Currency
|
||||
hide_item_qty: DF.Check
|
||||
items: DF.Table[ProformaInvoiceItem]
|
||||
letter_head: DF.Link | None
|
||||
naming_series: DF.Literal["PRO-.YYYY.-"]
|
||||
@@ -84,6 +85,7 @@ class ProformaInvoice(Document):
|
||||
sales_order.run_method("calculate_taxes_and_totals")
|
||||
sales_order.proforma_no = self.name
|
||||
sales_order.proforma_date = self.proforma_date
|
||||
sales_order.hide_item_qty = self.hide_item_qty
|
||||
self.db_set("grand_total", sales_order.grand_total)
|
||||
return frappe.attach_print(
|
||||
"Sales Order",
|
||||
@@ -138,6 +140,7 @@ def make_proforma_invoice(
|
||||
sales_order: str,
|
||||
items: str,
|
||||
based_on: str = "Quantity",
|
||||
hide_item_qty: bool | int = 0,
|
||||
naming_series: str | None = None,
|
||||
print_format: str | None = None,
|
||||
letter_head: str | None = None,
|
||||
@@ -145,7 +148,8 @@ def make_proforma_invoice(
|
||||
"""The sole creation path for a Proforma Invoice (the doctype is `in_create`).
|
||||
|
||||
`based_on` decides what the user edited per line: "Quantity" (rate fixed, amount = qty x rate)
|
||||
or "Amount" (qty fixed at ordered, rate derived so the line totals the entered amount).
|
||||
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)
|
||||
@@ -155,6 +159,7 @@ def make_proforma_invoice(
|
||||
proforma = frappe.new_doc("Proforma Invoice")
|
||||
proforma.sales_order = sales_order
|
||||
proforma.based_on = based_on
|
||||
proforma.hide_item_qty = 1 if (based_on == "Amount" and int(hide_item_qty or 0)) else 0
|
||||
if naming_series:
|
||||
proforma.naming_series = naming_series
|
||||
proforma.print_format = print_format or frappe.db.get_single_value(
|
||||
|
||||
@@ -112,6 +112,27 @@ class TestProformaInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(flt(data["proformed_qty"]), 3)
|
||||
self.assertEqual(flt(data["proformed_amount"]), 300)
|
||||
|
||||
def test_hide_item_qty_only_applies_to_amount_basis(self):
|
||||
sales_order = make_sales_order(qty=10)
|
||||
so_detail = sales_order.items[0].name
|
||||
|
||||
amount_based = make_proforma_invoice(
|
||||
sales_order.name,
|
||||
json.dumps([{"so_detail": so_detail, "qty": 5, "amount": 250}]),
|
||||
based_on="Amount",
|
||||
hide_item_qty=1,
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value("Proforma Invoice", amount_based, "hide_item_qty"), 1)
|
||||
|
||||
# ignored outside Amount basis
|
||||
qty_based = make_proforma_invoice(
|
||||
sales_order.name,
|
||||
json.dumps([{"so_detail": so_detail, "qty": 4}]),
|
||||
based_on="Quantity",
|
||||
hide_item_qty=1,
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value("Proforma Invoice", qty_based, "hide_item_qty"), 0)
|
||||
|
||||
def test_feature_toggle_is_enforced(self):
|
||||
sales_order = make_sales_order(qty=10)
|
||||
frappe.db.set_single_value("Selling Settings", "enable_proforma_invoice", 0)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"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<th class=\"text-right\" style=\"width: 14%;\">{{ _(\"Qty\") }}</th>\n\t\t\t\t<th class=\"text-right\" style=\"width: 16%;\">{{ _(\"Rate\") }}</th>\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<td class=\"text-right\">{{ row.get_formatted(\"qty\") }} {{ row.uom }}</td>\n\t\t\t\t<td class=\"text-right\">{{ row.get_formatted(\"rate\", doc) }}</td>\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\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",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 15.0,
|
||||
|
||||
Reference in New Issue
Block a user