mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-30 12:08:26 +00:00
Merge pull request #49508 from khushi8112/print-format-for-sales-invoice
feat: print format for sales invoice
This commit is contained in:
@@ -81,6 +81,7 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||
)
|
||||
|
||||
send_emails(process_soa.name, from_scheduler=True)
|
||||
process_soa.load_from_db()
|
||||
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
|
||||
@@ -279,6 +279,59 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def before_print(self, settings=None):
|
||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
||||
|
||||
super().before_print(settings)
|
||||
|
||||
company_details = frappe.get_value(
|
||||
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
||||
)
|
||||
|
||||
required_fields = [
|
||||
company_details.get("company_logo"),
|
||||
company_details.get("phone_no"),
|
||||
company_details.get("email"),
|
||||
]
|
||||
|
||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
address_display_list = get_address_display_list("Company", self.company)
|
||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
||||
|
||||
required_fields.append(self.company_address)
|
||||
required_fields.append(address_line)
|
||||
|
||||
if not all(required_fields):
|
||||
frappe.publish_realtime(
|
||||
"sales_invoice_before_print",
|
||||
{
|
||||
"company_logo": company_details.get("company_logo"),
|
||||
"website": company_details.get("website"),
|
||||
"phone_no": company_details.get("phone_no"),
|
||||
"email": company_details.get("email"),
|
||||
"address_line": address_line,
|
||||
"company": self.company,
|
||||
"company_address": self.company_address,
|
||||
"name": self.name,
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
@@ -2802,6 +2855,59 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_company_master_details(name, company, details):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if isinstance(details, str):
|
||||
details = frappe.parse_json(details)
|
||||
|
||||
if details.get("email"):
|
||||
validate_email_address(details.get("email"), throw=True)
|
||||
|
||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
||||
|
||||
if company_fields_to_update:
|
||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
||||
|
||||
company_address = details.get("company_address")
|
||||
if details.get("address_line1"):
|
||||
address_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": details.get("address_title"),
|
||||
"address_type": details.get("address_type"),
|
||||
"address_line1": details.get("address_line1"),
|
||||
"address_line2": details.get("address_line2"),
|
||||
"city": details.get("city"),
|
||||
"state": details.get("state"),
|
||||
"pincode": details.get("pincode"),
|
||||
"country": details.get("country"),
|
||||
"is_your_company_address": 1,
|
||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
||||
}
|
||||
)
|
||||
address_doc.insert()
|
||||
company_address = address_doc.name
|
||||
|
||||
if company_address:
|
||||
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
|
||||
if not company_address_display or details.get("address_line1"):
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
|
||||
(
|
||||
frappe.qb.update(SalesInvoice)
|
||||
.set(SalesInvoice.company_address, company_address)
|
||||
.set(SalesInvoice.company_address_display, get_address_display(company_address))
|
||||
.where(SalesInvoice.name == name)
|
||||
).run()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<style>
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
padding-right: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.letter-head td{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.invoice-header {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.logo-container {
|
||||
width: 90px;
|
||||
display: block;
|
||||
}
|
||||
.logo-container img {
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.company-details {
|
||||
width: 40%;
|
||||
align-content: center;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #171717;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invoice-info-cell {
|
||||
float: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
.invoice-info {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.invoice-label {
|
||||
color: #7C7C7C;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="invoice-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-cell" style="vertical-align: middle !important;">
|
||||
<div class="logo-container">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
|
||||
{% if company_logo %}
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="company-details">
|
||||
<div class="company-name">
|
||||
{{ doc.company }}
|
||||
</div>
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
|
||||
{{ company_address.get("address_line1") or "" }}<br>
|
||||
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
|
||||
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="invoice-info-cell">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Invoice:") }}</span>
|
||||
<span>{{ doc.name }}</span>
|
||||
</div>
|
||||
{% if company_details.website %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||
<span>{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||
<span>{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||
<span>{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<style>
|
||||
.print-format-preview {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
background: #f8f8f8;
|
||||
padding: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.letterhead-container {
|
||||
width: 100%;
|
||||
}
|
||||
.letterhead-container .other-details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.logo-address {
|
||||
width: 65%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.letter-head .logo {
|
||||
width: 90px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.letter-head .logo img {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: #171717;
|
||||
font-weight: bold;
|
||||
line-height: 23px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.company-address {
|
||||
color: #171717;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.invoice-number {
|
||||
color: #7c7c7c;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
color: #7c7c7c;
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
color: #171717;
|
||||
display: inline-block;
|
||||
}
|
||||
.letterhead-container td {
|
||||
padding: 0px !important;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="letterhead-container">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-address">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
|
||||
company_logo %}
|
||||
<div class="logo">
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="company-name">{{ doc.company }}</div>
|
||||
<div class="company-address">
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
{{ company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
|
||||
{{ company_address.city or "" }}, {{ company_address.state or "" }}
|
||||
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="vertical-align: top">
|
||||
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
||||
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
|
||||
<div class="invoice-number">{{ doc.name }}</div>
|
||||
<br />
|
||||
</div>
|
||||
<div style="text-align: left; float: right" class="other-details">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
{% if company_details.website %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Website:") }}</span
|
||||
><span class="contact-value">{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Email:") }}</span
|
||||
><span class="contact-value">{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Contact:") }}</span
|
||||
><span class="contact-value">{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -51,6 +51,8 @@ doctype_list_js = {
|
||||
],
|
||||
}
|
||||
|
||||
page_js = {"print": "public/js/print.js"}
|
||||
|
||||
extend_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}
|
||||
@@ -600,6 +602,7 @@ user_privacy_documents = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# ERPNext doctypes for Global Search
|
||||
global_search_doctypes = {
|
||||
"Default": [
|
||||
|
||||
145
erpnext/public/js/print.js
Normal file
145
erpnext/public/js/print.js
Normal file
@@ -0,0 +1,145 @@
|
||||
let beforePrintHandled = false;
|
||||
|
||||
frappe.realtime.on("sales_invoice_before_print", (data) => {
|
||||
const route = frappe.get_route();
|
||||
|
||||
if (!beforePrintHandled && route[0] === "print" && route[1] === "Sales Invoice") {
|
||||
beforePrintHandled = true;
|
||||
|
||||
let companyDetailsDialog = new frappe.ui.Dialog({
|
||||
title: "Enter Company Details",
|
||||
fields: [
|
||||
{
|
||||
label: "Company Logo",
|
||||
fieldname: "company_logo",
|
||||
fieldtype: "Attach Image",
|
||||
reqd: data.company_logo ? 0 : 1,
|
||||
hidden: data.company_logo ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Website",
|
||||
fieldname: "website",
|
||||
fieldtype: "Data",
|
||||
hidden: data.website ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Phone No",
|
||||
fieldname: "phone_no",
|
||||
fieldtype: "Data",
|
||||
reqd: data.phone_no ? 0 : 1,
|
||||
hidden: data.phone_no ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
fieldname: "email",
|
||||
fieldtype: "Data",
|
||||
options: "Email",
|
||||
reqd: data.email ? 0 : 1,
|
||||
hidden: data.email ? 1 : 0,
|
||||
},
|
||||
{
|
||||
fieldname: "section_break_1",
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
label: "Address Title",
|
||||
fieldname: "address_title",
|
||||
fieldtype: "Data",
|
||||
reqd: data.address_line ? 0 : 1,
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Address Type",
|
||||
fieldname: "address_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Billing", "Shipping"],
|
||||
default: "Billing",
|
||||
reqd: data.address_line ? 0 : 1,
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Address Line 1",
|
||||
fieldname: "address_line1",
|
||||
fieldtype: "Data",
|
||||
reqd: data.address_line ? 0 : 1,
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Address Line 2",
|
||||
fieldname: "address_line2",
|
||||
fieldtype: "Data",
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "City",
|
||||
fieldname: "city",
|
||||
fieldtype: "Data",
|
||||
reqd: data.address_line ? 0 : 1,
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "State",
|
||||
fieldname: "state",
|
||||
fieldtype: "Data",
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Country",
|
||||
fieldname: "country",
|
||||
fieldtype: "Link",
|
||||
options: "Country",
|
||||
reqd: data.address_line ? 0 : 1,
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Postal Code",
|
||||
fieldname: "pincode",
|
||||
fieldtype: "Data",
|
||||
hidden: data.address_line ? 1 : 0,
|
||||
},
|
||||
{
|
||||
label: "Select Company Address",
|
||||
fieldname: "company_address",
|
||||
fieldtype: "Link",
|
||||
options: "Address",
|
||||
get_query: function () {
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: {
|
||||
link_doctype: "Company",
|
||||
link_name: data.company,
|
||||
},
|
||||
};
|
||||
},
|
||||
reqd: data.address_line && !data.company_address ? 1 : 0,
|
||||
hidden: data.address_line && !data.company_address ? 0 : 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: "Save",
|
||||
primary_action(values) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.save_company_master_details",
|
||||
args: {
|
||||
name: data.name,
|
||||
company: data.company,
|
||||
details: values,
|
||||
},
|
||||
callback: function () {
|
||||
companyDetailsDialog.hide();
|
||||
frappe.msgprint(__("Updating details."));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
companyDetailsDialog.show();
|
||||
}
|
||||
});
|
||||
frappe.router.on("change", () => {
|
||||
const route = frappe.get_route();
|
||||
if (route[0] !== "print" || route[1] !== "Sales Invoice") {
|
||||
beforePrintHandled = false;
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
@@ -34,6 +36,7 @@ def after_install():
|
||||
update_roles()
|
||||
make_default_operations()
|
||||
update_pegged_currencies()
|
||||
create_letter_head()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@@ -279,6 +282,28 @@ def update_pegged_currencies():
|
||||
doc.save()
|
||||
|
||||
|
||||
def create_letter_head():
|
||||
base_path = frappe.get_app_path("erpnext", "accounts", "letterhead")
|
||||
|
||||
letterheads = {
|
||||
"Company Letterhead": "company_letterhead.html",
|
||||
"Company Letterhead - Grey": "company_letterhead_grey.html",
|
||||
}
|
||||
|
||||
for name, filename in letterheads.items():
|
||||
if not frappe.db.exists("Letter Head", name):
|
||||
content = frappe.read_file(os.path.join(base_path, filename))
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Letter Head",
|
||||
"letter_head_name": name,
|
||||
"source": "HTML",
|
||||
"content": content,
|
||||
}
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
DEFAULT_ROLE_PROFILES = {
|
||||
"Inventory": [
|
||||
"Stock User",
|
||||
|
||||
Reference in New Issue
Block a user