Merge pull request #28776 from rtdany10/ksa-vat-updates
feat(Regional): KSA E-Invoing optimizations and POS support
This commit is contained in:
commit
121f8ae865
@ -265,6 +265,9 @@ doc_events = {
|
||||
"erpnext.regional.india.utils.update_taxable_values"
|
||||
]
|
||||
},
|
||||
"POS Invoice": {
|
||||
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
"validate": [
|
||||
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
|
||||
|
@ -314,3 +314,5 @@ erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||
erpnext.patches.v14_0.delete_hub_doctypes
|
||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||
erpnext.patches.v14_0.migrate_crm_settings
|
||||
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others
|
||||
|
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2020, Wahni Green Technologies and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
|
||||
if company:
|
||||
return
|
||||
|
||||
if frappe.db.exists('DocType', 'Print Format'):
|
||||
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
|
||||
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
|
||||
for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
|
||||
frappe.db.set_value("Print Format", d, "disabled", 1)
|
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
16
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2020, Wahni Green Technologies and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
|
||||
if not company:
|
||||
return
|
||||
|
||||
if frappe.db.exists('DocType', 'Sales Invoice'):
|
||||
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
|
||||
if frappe.db.has_column('Sales Invoice', 'qr_code'):
|
||||
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2021-12-07 13:25:05.424827",
|
||||
"css": "",
|
||||
"custom_format": 1,
|
||||
"default_print_language": "en",
|
||||
"disabled": 1,
|
||||
"doc_type": "POS Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font_size": 0,
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 0.0,
|
||||
"margin_top": 0.0,
|
||||
"modified": "2021-12-08 10:25:01.930885",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "KSA POS Invoice",
|
||||
"owner": "Administrator",
|
||||
"page_number": "Hide",
|
||||
"print_format_builder": 0,
|
||||
"print_format_builder_beta": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.permissions import add_permission, update_permission_property
|
||||
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
|
||||
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
|
||||
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
@ -13,6 +13,16 @@ def setup(company=None, patch=True):
|
||||
add_permissions()
|
||||
make_custom_fields()
|
||||
|
||||
def add_print_formats():
|
||||
frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
|
||||
frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
|
||||
frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
|
||||
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
|
||||
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
|
||||
|
||||
for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
|
||||
frappe.db.set_value("Print Format", d, "disabled", 0)
|
||||
|
||||
def add_permissions():
|
||||
"""Add Permissions for KSA VAT Setting."""
|
||||
add_permission('KSA VAT Setting', 'All', 0)
|
||||
@ -33,8 +43,16 @@ def make_custom_fields():
|
||||
custom_fields = {
|
||||
'Sales Invoice': [
|
||||
dict(
|
||||
fieldname='qr_code',
|
||||
label='QR Code',
|
||||
fieldname='ksa_einv_qr',
|
||||
label='KSA E-Invoicing QR',
|
||||
fieldtype='Attach Image',
|
||||
read_only=1, no_copy=1, hidden=1
|
||||
)
|
||||
],
|
||||
'POS Invoice': [
|
||||
dict(
|
||||
fieldname='ksa_einv_qr',
|
||||
label='KSA E-Invoicing QR',
|
||||
fieldtype='Attach Image',
|
||||
read_only=1, no_copy=1, hidden=1
|
||||
)
|
||||
|
@ -4,144 +4,146 @@ from base64 import b64encode
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils.data import add_to_date, get_time, getdate
|
||||
from pyqrcode import create as qr_create
|
||||
|
||||
from erpnext import get_region
|
||||
|
||||
|
||||
def create_qr_code(doc, method):
|
||||
"""Create QR Code after inserting Sales Inv
|
||||
"""
|
||||
|
||||
def create_qr_code(doc, method=None):
|
||||
region = get_region(doc.company)
|
||||
if region not in ['Saudi Arabia']:
|
||||
return
|
||||
|
||||
# if QR Code field not present, do nothing
|
||||
if not hasattr(doc, 'qr_code'):
|
||||
return
|
||||
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
|
||||
if not hasattr(doc, 'ksa_einv_qr'):
|
||||
create_custom_fields({
|
||||
doc.doctype: [
|
||||
dict(
|
||||
fieldname='ksa_einv_qr',
|
||||
label='KSA E-Invoicing QR',
|
||||
fieldtype='Attach Image',
|
||||
read_only=1, no_copy=1, hidden=1
|
||||
)
|
||||
]
|
||||
})
|
||||
|
||||
# Don't create QR Code if it already exists
|
||||
qr_code = doc.get("qr_code")
|
||||
qr_code = doc.get("ksa_einv_qr")
|
||||
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
|
||||
return
|
||||
|
||||
meta = frappe.get_meta('Sales Invoice')
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
|
||||
for field in meta.get_image_fields():
|
||||
if field.fieldname == 'qr_code':
|
||||
''' TLV conversion for
|
||||
1. Seller's Name
|
||||
2. VAT Number
|
||||
3. Time Stamp
|
||||
4. Invoice Amount
|
||||
5. VAT Amount
|
||||
'''
|
||||
tlv_array = []
|
||||
# Sellers Name
|
||||
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
|
||||
''' TLV conversion for
|
||||
1. Seller's Name
|
||||
2. VAT Number
|
||||
3. Time Stamp
|
||||
4. Invoice Amount
|
||||
5. VAT Amount
|
||||
'''
|
||||
tlv_array = []
|
||||
# Sellers Name
|
||||
|
||||
seller_name = frappe.db.get_value(
|
||||
'Company',
|
||||
doc.company,
|
||||
'company_name_in_arabic')
|
||||
seller_name = frappe.db.get_value(
|
||||
'Company',
|
||||
doc.company,
|
||||
'company_name_in_arabic')
|
||||
|
||||
if not seller_name:
|
||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||
if not seller_name:
|
||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||
|
||||
tag = bytes([1]).hex()
|
||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||
value = seller_name.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([1]).hex()
|
||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||
value = seller_name.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# VAT Number
|
||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||
if not tax_id:
|
||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||
# VAT Number
|
||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||
if not tax_id:
|
||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||
|
||||
tag = bytes([2]).hex()
|
||||
length = bytes([len(tax_id)]).hex()
|
||||
value = tax_id.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([2]).hex()
|
||||
length = bytes([len(tax_id)]).hex()
|
||||
value = tax_id.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Time Stamp
|
||||
posting_date = getdate(doc.posting_date)
|
||||
time = get_time(doc.posting_time)
|
||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
# Time Stamp
|
||||
posting_date = getdate(doc.posting_date)
|
||||
time = get_time(doc.posting_time)
|
||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
tag = bytes([3]).hex()
|
||||
length = bytes([len(time_stamp)]).hex()
|
||||
value = time_stamp.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([3]).hex()
|
||||
length = bytes([len(time_stamp)]).hex()
|
||||
value = time_stamp.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Invoice Amount
|
||||
invoice_amount = str(doc.grand_total)
|
||||
tag = bytes([4]).hex()
|
||||
length = bytes([len(invoice_amount)]).hex()
|
||||
value = invoice_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
# Invoice Amount
|
||||
invoice_amount = str(doc.grand_total)
|
||||
tag = bytes([4]).hex()
|
||||
length = bytes([len(invoice_amount)]).hex()
|
||||
value = invoice_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# VAT Amount
|
||||
vat_amount = str(doc.total_taxes_and_charges)
|
||||
# VAT Amount
|
||||
vat_amount = str(doc.total_taxes_and_charges)
|
||||
|
||||
tag = bytes([5]).hex()
|
||||
length = bytes([len(vat_amount)]).hex()
|
||||
value = vat_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
tag = bytes([5]).hex()
|
||||
length = bytes([len(vat_amount)]).hex()
|
||||
value = vat_amount.encode('utf-8').hex()
|
||||
tlv_array.append(''.join([tag, length, value]))
|
||||
|
||||
# Joining bytes into one
|
||||
tlv_buff = ''.join(tlv_array)
|
||||
# Joining bytes into one
|
||||
tlv_buff = ''.join(tlv_array)
|
||||
|
||||
# base64 conversion for QR Code
|
||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||
# base64 conversion for QR Code
|
||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||
|
||||
qr_image = io.BytesIO()
|
||||
url = qr_create(base64_string, error='L')
|
||||
url.png(qr_image, scale=2, quiet_zone=1)
|
||||
qr_image = io.BytesIO()
|
||||
url = qr_create(base64_string, error='L')
|
||||
url.png(qr_image, scale=2, quiet_zone=1)
|
||||
|
||||
name = frappe.generate_hash(doc.name, 5)
|
||||
name = frappe.generate_hash(doc.name, 5)
|
||||
|
||||
# making file
|
||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"is_private": 0,
|
||||
"content": qr_image.getvalue(),
|
||||
"attached_to_doctype": doc.get("doctype"),
|
||||
"attached_to_name": doc.get("name"),
|
||||
"attached_to_field": "qr_code"
|
||||
})
|
||||
# making file
|
||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||
_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
"is_private": 0,
|
||||
"content": qr_image.getvalue(),
|
||||
"attached_to_doctype": doc.get("doctype"),
|
||||
"attached_to_name": doc.get("name"),
|
||||
"attached_to_field": "ksa_einv_qr"
|
||||
})
|
||||
|
||||
_file.save()
|
||||
_file.save()
|
||||
|
||||
# assigning to document
|
||||
doc.db_set('qr_code', _file.file_url)
|
||||
doc.notify_update()
|
||||
|
||||
break
|
||||
# assigning to document
|
||||
doc.db_set('ksa_einv_qr', _file.file_url)
|
||||
doc.notify_update()
|
||||
|
||||
|
||||
def delete_qr_code_file(doc, method):
|
||||
"""Delete QR Code on deleted sales invoice"""
|
||||
|
||||
def delete_qr_code_file(doc, method=None):
|
||||
region = get_region(doc.company)
|
||||
if region not in ['Saudi Arabia']:
|
||||
return
|
||||
|
||||
if hasattr(doc, 'qr_code'):
|
||||
if doc.get('qr_code'):
|
||||
if hasattr(doc, 'ksa_einv_qr'):
|
||||
if doc.get('ksa_einv_qr'):
|
||||
file_doc = frappe.get_list('File', {
|
||||
'file_url': doc.get('qr_code')
|
||||
'file_url': doc.get('ksa_einv_qr')
|
||||
})
|
||||
if len(file_doc):
|
||||
frappe.delete_doc('File', file_doc[0].name)
|
||||
|
||||
def delete_vat_settings_for_company(doc, method):
|
||||
def delete_vat_settings_for_company(doc, method=None):
|
||||
if doc.country != 'Saudi Arabia':
|
||||
return
|
||||
|
||||
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
|
||||
settings_doc.delete()
|
||||
if frappe.db.exists('KSA VAT Setting', doc.name):
|
||||
frappe.delete_doc('KSA VAT Setting', doc.name)
|
||||
|
Loading…
Reference in New Issue
Block a user