Merge pull request #28776 from rtdany10/ksa-vat-updates

feat(Regional): KSA E-Invoing optimizations and POS support
This commit is contained in:
Deepesh Garg 2021-12-08 22:32:24 +05:30 committed by GitHub
commit 121f8ae865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 100 deletions

View File

@ -265,6 +265,9 @@ doc_events = {
"erpnext.regional.india.utils.update_taxable_values" "erpnext.regional.india.utils.update_taxable_values"
] ]
}, },
"POS Invoice": {
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
},
"Purchase Invoice": { "Purchase Invoice": {
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction", "erpnext.regional.india.utils.validate_reverse_charge_transaction",

View File

@ -314,3 +314,5 @@ erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v14_0.migrate_crm_settings 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

View 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)

View 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')

View File

@ -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

View File

@ -3,7 +3,7 @@
import frappe import frappe
from frappe.permissions import add_permission, update_permission_property 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 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 from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@ -13,6 +13,16 @@ def setup(company=None, patch=True):
add_permissions() add_permissions()
make_custom_fields() 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(): def add_permissions():
"""Add Permissions for KSA VAT Setting.""" """Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0) add_permission('KSA VAT Setting', 'All', 0)
@ -33,8 +43,16 @@ def make_custom_fields():
custom_fields = { custom_fields = {
'Sales Invoice': [ 'Sales Invoice': [
dict( dict(
fieldname='qr_code', fieldname='ksa_einv_qr',
label='QR Code', 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', fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1 read_only=1, no_copy=1, hidden=1
) )

View File

@ -4,144 +4,146 @@ from base64 import b64encode
import frappe import frappe
from frappe import _ 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 frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create from pyqrcode import create as qr_create
from erpnext import get_region from erpnext import get_region
def create_qr_code(doc, method): def create_qr_code(doc, method=None):
"""Create QR Code after inserting Sales Inv
"""
region = get_region(doc.company) region = get_region(doc.company)
if region not in ['Saudi Arabia']: if region not in ['Saudi Arabia']:
return return
# if QR Code field not present, do nothing # if QR Code field not present, create it. Invoices without QR are invalid as per law.
if not hasattr(doc, 'qr_code'): if not hasattr(doc, 'ksa_einv_qr'):
return 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 # 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}): if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return return
meta = frappe.get_meta('Sales Invoice') meta = frappe.get_meta(doc.doctype)
for field in meta.get_image_fields(): if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
if field.fieldname == 'qr_code': ''' TLV conversion for
''' TLV conversion for 1. Seller's Name
1. Seller's Name 2. VAT Number
2. VAT Number 3. Time Stamp
3. Time Stamp 4. Invoice Amount
4. Invoice Amount 5. VAT Amount
5. VAT Amount '''
''' tlv_array = []
tlv_array = [] # Sellers Name
# Sellers Name
seller_name = frappe.db.get_value( seller_name = frappe.db.get_value(
'Company', 'Company',
doc.company, doc.company,
'company_name_in_arabic') 'company_name_in_arabic')
if not seller_name: if not seller_name:
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company)) frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
tag = bytes([1]).hex() tag = bytes([1]).hex()
length = bytes([len(seller_name.encode('utf-8'))]).hex() length = bytes([len(seller_name.encode('utf-8'))]).hex()
value = seller_name.encode('utf-8').hex() value = seller_name.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# VAT Number # VAT Number
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id') tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
if not tax_id: if not tax_id:
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company)) frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
tag = bytes([2]).hex() tag = bytes([2]).hex()
length = bytes([len(tax_id)]).hex() length = bytes([len(tax_id)]).hex()
value = tax_id.encode('utf-8').hex() value = tax_id.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Time Stamp # Time Stamp
posting_date = getdate(doc.posting_date) posting_date = getdate(doc.posting_date)
time = get_time(doc.posting_time) time = get_time(doc.posting_time)
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
time_stamp = add_to_date(posting_date, seconds=seconds) time_stamp = add_to_date(posting_date, seconds=seconds)
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ') time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
tag = bytes([3]).hex() tag = bytes([3]).hex()
length = bytes([len(time_stamp)]).hex() length = bytes([len(time_stamp)]).hex()
value = time_stamp.encode('utf-8').hex() value = time_stamp.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Invoice Amount # Invoice Amount
invoice_amount = str(doc.grand_total) invoice_amount = str(doc.grand_total)
tag = bytes([4]).hex() tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex() length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode('utf-8').hex() value = invoice_amount.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# VAT Amount # VAT Amount
vat_amount = str(doc.total_taxes_and_charges) vat_amount = str(doc.total_taxes_and_charges)
tag = bytes([5]).hex() tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex() length = bytes([len(vat_amount)]).hex()
value = vat_amount.encode('utf-8').hex() value = vat_amount.encode('utf-8').hex()
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Joining bytes into one # Joining bytes into one
tlv_buff = ''.join(tlv_array) tlv_buff = ''.join(tlv_array)
# base64 conversion for QR Code # base64 conversion for QR Code
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode() base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
qr_image = io.BytesIO() qr_image = io.BytesIO()
url = qr_create(base64_string, error='L') url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1) 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 # making file
filename = f"QRCode-{name}.png".replace(os.path.sep, "__") filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({ _file = frappe.get_doc({
"doctype": "File", "doctype": "File",
"file_name": filename, "file_name": filename,
"is_private": 0, "is_private": 0,
"content": qr_image.getvalue(), "content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"), "attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"), "attached_to_name": doc.get("name"),
"attached_to_field": "qr_code" "attached_to_field": "ksa_einv_qr"
}) })
_file.save() _file.save()
# assigning to document # assigning to document
doc.db_set('qr_code', _file.file_url) doc.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update() doc.notify_update()
break
def delete_qr_code_file(doc, method): def delete_qr_code_file(doc, method=None):
"""Delete QR Code on deleted sales invoice"""
region = get_region(doc.company) region = get_region(doc.company)
if region not in ['Saudi Arabia']: if region not in ['Saudi Arabia']:
return return
if hasattr(doc, 'qr_code'): if hasattr(doc, 'ksa_einv_qr'):
if doc.get('qr_code'): if doc.get('ksa_einv_qr'):
file_doc = frappe.get_list('File', { file_doc = frappe.get_list('File', {
'file_url': doc.get('qr_code') 'file_url': doc.get('ksa_einv_qr')
}) })
if len(file_doc): if len(file_doc):
frappe.delete_doc('File', file_doc[0].name) 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': if doc.country != 'Saudi Arabia':
return return
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name}) if frappe.db.exists('KSA VAT Setting', doc.name):
settings_doc.delete() frappe.delete_doc('KSA VAT Setting', doc.name)