Merge pull request #27369 from Havenir/ksa-vat

feat(regional): KSA E-Invoicing and VAT Report
This commit is contained in:
Deepesh Garg 2021-10-11 12:16:51 +05:30 committed by GitHub
commit 1aba3592fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 709 additions and 6 deletions

View File

@ -533,6 +533,17 @@
"only_for": "United Arab Emirates",
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1153,6 +1164,16 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1206,7 +1227,7 @@
"type": "Link"
}
],
"modified": "2021-08-27 12:15:52.872470",
"modified": "2021-08-26 13:15:52.872470",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",

View File

@ -250,6 +250,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
"after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
@ -259,7 +260,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
"on_trash": "erpnext.regional.check_deletion_permission",
"on_trash": [
"erpnext.regional.check_deletion_permission",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"

View File

@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
"document_name": doc.name,
"data": data
}).insert(ignore_permissions=True)

View File

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 09:17:09.862163",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:38.205597",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Purchase Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATPurchaseAccount(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Sales Account', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 08:46:33.820968",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:00.081407",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Sales Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSalesAccount(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSalesAccount(unittest.TestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Setting', {
onload: function () {
frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
}
});

View File

@ -0,0 +1,49 @@
{
"actions": [],
"autoname": "field:company",
"creation": "2021-07-13 08:49:01.100356",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"ksa_vat_sales_accounts",
"ksa_vat_purchase_accounts"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"unique": 1
},
{
"fieldname": "ksa_vat_sales_accounts",
"fieldtype": "Table",
"label": "KSA VAT Sales Accounts",
"options": "KSA VAT Sales Account",
"reqd": 1
},
{
"fieldname": "ksa_vat_purchase_accounts",
"fieldtype": "Table",
"label": "KSA VAT Purchase Accounts",
"options": "KSA VAT Purchase Account",
"reqd": 1
}
],
"links": [],
"modified": "2021-08-26 04:29:06.499378",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Setting",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSetting(Document):
pass

View File

@ -0,0 +1,5 @@
frappe.listview_settings['KSA VAT Setting'] = {
onload () {
frappe.breadcrumbs.add('Accounts');
}
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSetting(unittest.TestCase):
pass

View File

@ -0,0 +1,60 @@
// Copyright (c) 2016, Havenir Solutions and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["KSA VAT"] = {
onload() {
frappe.breadcrumbs.add('Accounts');
},
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.get_today()
}
],
"formatter": function(value, row, column, data, default_formatter) {
if (data
&& (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
&& data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else if (data.title=='Grand Total'){
if (data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else{
value = default_formatter(value, row, column, data);
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
console.log($value)
return value
}
}else{
value = default_formatter(value, row, column, data);
return value;
}
},
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-13 08:54:38.000949",
"disable_prepared_report": 1,
"disabled": 1,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-08-26 04:14:37.202594",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT",
"owner": "Administrator",
"prepared_report": 1,
"ref_doctype": "GL Entry",
"report_name": "KSA VAT",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@ -0,0 +1,176 @@
# Copyright (c) 2013, Havenir Solutions and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import frappe
from frappe import _
from frappe.utils import get_url_to_list
def execute(filters=None):
columns = columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
"width": 300
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
"width": 150,
}
]
def get_data(filters):
data = []
# Validate if vat settings exist
company = filters.get('company')
if frappe.db.exists('KSA VAT Setting', company) is None:
url = get_url_to_list('KSA VAT Setting')
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
return data
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
# Sales Heading
append_data(data, 'VAT on Sales', '', '', '')
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
total_taxable_amount, total_taxable_adjustment_amount, \
total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice')
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Sales Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
# Blank Line
append_data(data, '', '', '', '')
# Purchase Heading
append_data(data, 'VAT on Purchases', '', '', '')
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
total_taxable_amount, total_taxable_adjustment_amount, \
total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice')
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Purchase Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
return data
def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
'''
(KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
calculates and returns \n
total_taxable_amount, total_taxable_adjustment_amount, total_tax'''
from_date = filters.get('from_date')
to_date = filters.get('to_date')
# Initiate variables
total_taxable_amount = 0
total_taxable_adjustment_amount = 0
total_tax = 0
# Fetch All Invoices
invoices = frappe.get_list(doctype,
filters ={
'docstatus': 1,
'posting_date': ['between', [from_date, to_date]]
}, fields =['name', 'is_return'])
for invoice in invoices:
invoice_items = frappe.get_list(f'{doctype} Item',
filters ={
'docstatus': 1,
'parent': invoice.name,
'item_tax_template': vat_setting.item_tax_template
}, fields =['item_code', 'net_amount'])
for item in invoice_items:
# Summing up total taxable amount
if invoice.is_return == 0:
total_taxable_amount += item.net_amount
if invoice.is_return == 1:
total_taxable_adjustment_amount += item.net_amount
# Summing up total tax
total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
return total_taxable_amount, total_taxable_adjustment_amount, total_tax
def append_data(data, title, amount, adjustment_amount, vat_amount):
"""Returns data with appended value."""
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == 'Sales Invoice':
tax_doctype = 'Sales Taxes and Charges'
elif doctype == 'Purchase Invoice':
tax_doctype = 'Purchase Taxes and Charges'
item_wise_tax_detail = frappe.get_value(tax_doctype, {
'docstatus': 1,
'parent': parent,
'account_head': account_head
}, 'item_wise_tax_detail')
tax_amount = 0
if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
for key, value in item_wise_tax_detail.items():
if key == item_code:
tax_amount = value[1]
break
return tax_amount

View File

@ -2,10 +2,36 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
from erpnext.regional.united_arab_emirates.setup import make_custom_fields, add_print_formats
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.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def setup(company=None, patch=True):
make_custom_fields()
uae_custom_fields()
add_print_formats()
add_permissions()
create_ksa_vat_setting(company)
make_qrcode_field()
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission('KSA VAT Setting', role, 0)
update_permission_property('KSA VAT Setting', role, 0, 'write', 1)
update_permission_property('KSA VAT Setting', role, 0, 'create', 1)
"""Enable KSA VAT Report"""
frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0)
def make_qrcode_field():
"""Created QR code Image file"""
qr_code_field = dict(
fieldname='qr_code',
label='QR Code',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1)
create_custom_field('Sales Invoice', qr_code_field)

View File

@ -0,0 +1,77 @@
import io
import os
import frappe
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
"""
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
# Don't create QR Code if it already exists
qr_code = doc.get("qr_code")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
meta = frappe.get_meta('Sales Invoice')
for field in meta.get_image_fields():
if field.fieldname == 'qr_code':
# Creating public url to print format
default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
# System Language
language = frappe.get_system_settings('language')
# creating qr code for the url
url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
qr_image = io.BytesIO()
url = qr_create(url, error='L')
url.png(qr_image, scale=2, quiet_zone=1)
# making file
filename = f"QR-CODE-{doc.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"
})
_file.save()
# assigning to document
doc.db_set('qr_code', _file.file_url)
doc.notify_update()
break
def delete_qr_code_file(doc, method):
"""Delete QR Code on deleted sales invoice"""
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
if hasattr(doc, 'qr_code'):
if doc.get('qr_code'):
file_doc = frappe.get_list('File', {
'file_url': doc.get('qr_code')
})
if len(file_doc):
frappe.delete_doc('File', file_doc[0].name)

View File

@ -0,0 +1,47 @@
[
{
"type": "Sales Account",
"accounts": [
{
"title": "Standard rated Sales",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Zero rated domestic sales",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted sales",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Zero"
}
]
},
{
"type": "Purchase Account",
"accounts": [
{
"title": "Standard rated domestic purchases",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Imports subject to VAT paid at customs",
"item_tax_template": "KSA Excise 50%",
"account": "Excise 50%"
},
{
"title": "Zero rated purchases",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted purchases",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Zero"
}
]
}
]

View File

@ -0,0 +1,46 @@
import json
import os
import frappe
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
def create_ksa_vat_setting(company):
"""On creation of first company. Creates KSA VAT Setting"""
company = frappe.get_doc('Company', company)
setup_taxes_and_charges(company.name, company.country)
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json')
with open(file_path, 'r') as json_file:
account_data = json.load(json_file)
# Creating KSA VAT Setting
ksa_vat_setting = frappe.get_doc({
'doctype': 'KSA VAT Setting',
'company': company.name
})
for data in account_data:
if data['type'] == 'Sales Account':
for row in data['accounts']:
item_tax_template = row['item_tax_template']
account = row['account']
ksa_vat_setting.append('ksa_vat_sales_accounts', {
'title': row['title'],
'item_tax_template': f'{item_tax_template} - {company.abbr}',
'account': f'{account} - {company.abbr}'
})
elif data['type'] == 'Purchase Account':
for row in data['accounts']:
item_tax_template = row['item_tax_template']
account = row['account']
ksa_vat_setting.append('ksa_vat_purchase_accounts', {
'title': row['title'],
'item_tax_template': f'{item_tax_template} - {company.abbr}',
'account': f'{account} - {company.abbr}'
})
ksa_vat_setting.save()