Merge branch 'develop' into gstr_1_taxable_value_update
This commit is contained in:
commit
1f49335335
@ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue
|
||||
from frappe.model.mapper import map_doc, map_child_doc
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
import json
|
||||
|
||||
from six import iteritems
|
||||
|
||||
@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
@ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document):
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
|
||||
return credit_note.name
|
||||
@ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document):
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
found = True
|
||||
if not found:
|
||||
tax.charge_type = 'Actual'
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||
taxes.append(tax)
|
||||
|
||||
for payment in doc.get('payments'):
|
||||
@ -168,11 +177,9 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = frappe.new_doc('Sales Invoice')
|
||||
sales_invoice.customer = self.customer
|
||||
sales_invoice.is_pos = 1
|
||||
# date can be pos closing date?
|
||||
sales_invoice.posting_date = getdate(nowdate())
|
||||
|
||||
return sales_invoice
|
||||
|
||||
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
|
||||
si.flags.ignore_validate = True
|
||||
si.cancel()
|
||||
|
||||
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
|
||||
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
|
||||
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
|
||||
|
||||
if not consolidated_tax_detail:
|
||||
consolidated_tax_detail = {}
|
||||
|
||||
for item_code, tax_data in tax_row_detail.items():
|
||||
if consolidated_tax_detail.get(item_code):
|
||||
consolidated_tax_data = consolidated_tax_detail.get(item_code)
|
||||
consolidated_tax_detail.update({
|
||||
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
|
||||
})
|
||||
else:
|
||||
consolidated_tax_detail.update({
|
||||
item_code: [tax_data[0], tax_data[1]]
|
||||
})
|
||||
|
||||
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
|
||||
|
||||
def get_all_unconsolidated_invoices():
|
||||
filters = {
|
||||
'consolidated_invoice': [ 'in', [ '', None ]],
|
||||
@ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
|
||||
|
||||
if len(invoices) >= 5 and closing_entry:
|
||||
closing_entry.set_status(update=True, status='Queued')
|
||||
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
|
||||
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
|
||||
else:
|
||||
create_merge_logs(invoice_by_customer, closing_entry)
|
||||
|
||||
@ -227,21 +254,21 @@ def unconsolidate_pos_invoices(closing_entry):
|
||||
|
||||
if len(merge_logs) >= 5:
|
||||
closing_entry.set_status(update=True, status='Queued')
|
||||
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
|
||||
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
|
||||
else:
|
||||
cancel_merge_logs(merge_logs, closing_entry)
|
||||
|
||||
def create_merge_logs(invoice_by_customer, closing_entry={}):
|
||||
for customer, invoices in iteritems(invoice_by_customer):
|
||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||
merge_log.posting_date = getdate(nowdate())
|
||||
merge_log.posting_date = getdate(closing_entry.get('posting_date'))
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name', None)
|
||||
|
||||
merge_log.set('pos_invoices', invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Submitted')
|
||||
closing_entry.update_opening_entry()
|
||||
@ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
|
||||
closing_entry.set_status(update=True, status='Cancelled')
|
||||
closing_entry.update_opening_entry(for_cancel=True)
|
||||
|
||||
def enqueue_job(job, invoice_by_customer, closing_entry):
|
||||
def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
|
||||
check_scheduler_status()
|
||||
|
||||
job_name = closing_entry.get("name")
|
||||
@ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry):
|
||||
job_name=job_name,
|
||||
closing_entry=closing_entry,
|
||||
invoice_by_customer=invoice_by_customer,
|
||||
merge_logs=merge_logs,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
import json
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
|
||||
@ -99,4 +100,51 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidated_invoice_item_taxes(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 9
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
inv2.get('items')[0].item_code = '_Test Item 2'
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 5
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
inv.load_from_db()
|
||||
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
|
||||
|
||||
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
|
||||
self.assertEqual(tax_rate, 9)
|
||||
self.assertEqual(amount, 9)
|
||||
|
||||
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
|
||||
self.assertEqual(tax_rate2, 5)
|
||||
self.assertEqual(amount2, 5)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
var globalOnload = frappe.listview_settings['Sales Invoice'].onload;
|
||||
frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
|
||||
frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
|
||||
|
||||
// Provision in case onload event is added to sales_invoice.js in future
|
||||
if (globalOnload) {
|
||||
globalOnload(doclist);
|
||||
globalOnload(list_view);
|
||||
}
|
||||
|
||||
const action = () => {
|
||||
const selected_docs = doclist.get_checked_items();
|
||||
const docnames = doclist.get_checked_items(true);
|
||||
const selected_docs = list_view.get_checked_items();
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
|
||||
for (let doc of selected_docs) {
|
||||
if (doc.docstatus !== 1) {
|
||||
@ -19,7 +19,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.utils.generate_ewb_json',
|
||||
args: {
|
||||
'dt': doclist.doctype,
|
||||
'dt': list_view.doctype,
|
||||
'dn': docnames
|
||||
},
|
||||
callback: function(r) {
|
||||
@ -35,5 +35,140 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
|
||||
});
|
||||
};
|
||||
|
||||
doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
||||
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
||||
|
||||
const generate_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
if (docnames && docnames.length) {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
|
||||
args: { docnames },
|
||||
freeze: true,
|
||||
freeze_message: __('Generating E-Invoices...')
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
message: __('Please select at least one sales invoice to generate IRN'),
|
||||
title: __('No Invoice Selected'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancel_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Cancel IRN"),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
|
||||
args: {
|
||||
doctype: list_view.doctype,
|
||||
docnames,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Cancelling E-Invoices...'),
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
|
||||
let einvoicing_enabled = false;
|
||||
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
|
||||
einvoicing_enabled = enabled;
|
||||
});
|
||||
|
||||
list_view.$result.on("change", "input[type=checkbox]", () => {
|
||||
if (einvoicing_enabled) {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
// show/hide e-invoicing actions when no sales invoices are checked
|
||||
if (docnames && docnames.length) {
|
||||
// prevent adding actions twice if e-invoicing action group already exists
|
||||
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
|
||||
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
|
||||
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
|
||||
}
|
||||
} else {
|
||||
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
|
||||
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices generated successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to generate IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to cancel IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -916,7 +916,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message){
|
||||
data = r.message;
|
||||
let data = r.message;
|
||||
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
|
||||
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);
|
||||
|
@ -147,7 +147,9 @@ class calculate_taxes_and_totals(object):
|
||||
validate_taxes_and_charges(tax)
|
||||
validate_inclusive_tax(tax, self.doc)
|
||||
|
||||
tax.item_wise_tax_detail = {}
|
||||
if not self.doc.get('is_consolidated'):
|
||||
tax.item_wise_tax_detail = {}
|
||||
|
||||
tax_fields = ["total", "tax_amount_after_discount_amount",
|
||||
"tax_amount_for_current_item", "grand_total_for_current_item",
|
||||
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
|
||||
@ -338,7 +340,9 @@ class calculate_taxes_and_totals(object):
|
||||
current_tax_amount = tax_rate * item.qty
|
||||
|
||||
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
|
||||
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
|
||||
|
||||
if not self.doc.get("is_consolidated"):
|
||||
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
|
||||
|
||||
return current_tax_amount
|
||||
|
||||
@ -440,8 +444,9 @@ class calculate_taxes_and_totals(object):
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
def _cleanup(self):
|
||||
for tax in self.doc.get("taxes"):
|
||||
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
|
||||
if not self.doc.get('is_consolidated'):
|
||||
for tax in self.doc.get("taxes"):
|
||||
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
|
||||
|
||||
def set_discount_amount(self):
|
||||
if self.doc.additional_discount_percentage:
|
||||
|
@ -262,7 +262,8 @@ doc_events = {
|
||||
],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||
"validate": [
|
||||
"erpnext.regional.india.utils.validate_document_name"
|
||||
"erpnext.regional.india.utils.validate_document_name",
|
||||
"erpnext.regional.india.utils.update_taxable_values"
|
||||
]
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
|
@ -93,15 +93,15 @@ class TestBOM(unittest.TestCase):
|
||||
base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
|
||||
base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
|
||||
|
||||
# test amounts in selected currency
|
||||
self.assertEqual(bom.operating_cost, op_cost)
|
||||
self.assertEqual(bom.raw_material_cost, raw_material_cost)
|
||||
self.assertEqual(bom.total_cost, raw_material_cost + op_cost)
|
||||
# test amounts in selected currency, almostEqual checks for 7 digits by default
|
||||
self.assertAlmostEqual(bom.operating_cost, op_cost)
|
||||
self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
|
||||
self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
|
||||
|
||||
# test amounts in selected currency
|
||||
self.assertEqual(bom.base_operating_cost, base_op_cost)
|
||||
self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||
self.assertEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||
self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
|
||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||
|
||||
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
|
||||
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
|
||||
|
@ -756,12 +756,16 @@ erpnext.patches.v13_0.update_payment_terms_outstanding
|
||||
erpnext.patches.v12_0.add_state_code_for_ladakh
|
||||
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
|
||||
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
|
||||
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
|
||||
erpnext.patches.v12_0.update_vehicle_no_reqd_condition
|
||||
erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
|
||||
erpnext.patches.v12_0.add_einvoice_summary_report_permissions
|
||||
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
|
||||
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
|
||||
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
|
||||
erpnext.patches.v13_0.setup_uae_vat_fields
|
||||
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
|
||||
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||
erpnext.patches.v13_0.rename_discharge_date_in_ip_record
|
||||
erpnext.patches.v12_0.create_taxable_value_field
|
||||
erpnext.patches.v12_0.add_gst_category_in_delivery_note
|
||||
erpnext.patches.v12_0.purchase_receipt_status
|
||||
|
@ -0,0 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||
if not company or not frappe.db.count('E Invoice User'):
|
||||
return
|
||||
|
||||
frappe.reload_doc("regional", "doctype", "e_invoice_user")
|
||||
for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
|
||||
company_name = frappe.db.sql("""
|
||||
select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
|
||||
where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
|
||||
""", (creds.get('gstin')))
|
||||
if company_name and len(company_name) == 1:
|
||||
frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
|
69
erpnext/patches/v12_0/add_einvoice_status_field.py
Normal file
69
erpnext/patches/v12_0/add_einvoice_status_field.py
Normal file
@ -0,0 +1,69 @@
|
||||
from __future__ import unicode_literals
|
||||
import json
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||
if not company:
|
||||
return
|
||||
|
||||
# move hidden einvoice fields to a different section
|
||||
custom_fields = {
|
||||
'Sales Invoice': [
|
||||
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
|
||||
print_hide=1, hidden=1),
|
||||
|
||||
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
|
||||
options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
|
||||
hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
|
||||
]
|
||||
}
|
||||
create_custom_fields(custom_fields, update=True)
|
||||
|
||||
if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
|
||||
frappe.db.sql('''
|
||||
UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
|
||||
WHERE
|
||||
posting_date >= '2021-04-01'
|
||||
AND ifnull(irn, '') = ''
|
||||
AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
|
||||
AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
|
||||
''')
|
||||
|
||||
# set appropriate statuses
|
||||
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
|
||||
WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
|
||||
|
||||
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
|
||||
WHERE ifnull(irn_cancelled, 0) = 1''')
|
||||
|
||||
# set correct acknowledgement in e-invoices
|
||||
einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
|
||||
|
||||
if einvoices:
|
||||
for inv in einvoices:
|
||||
signed_einvoice = inv.get('signed_einvoice')
|
||||
if signed_einvoice:
|
||||
signed_einvoice = json.loads(signed_einvoice)
|
||||
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
|
||||
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)
|
@ -0,0 +1,18 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||
if not company:
|
||||
return
|
||||
|
||||
if frappe.db.exists('Report', 'E-Invoice Summary') and \
|
||||
not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
|
||||
frappe.get_doc(dict(
|
||||
doctype='Custom Role',
|
||||
report='E-Invoice Summary',
|
||||
roles= [
|
||||
dict(role='Accounts User'),
|
||||
dict(role='Accounts Manager')
|
||||
]
|
||||
)).insert()
|
18
erpnext/patches/v12_0/create_taxable_value_field.py
Normal file
18
erpnext/patches/v12_0/create_taxable_value_field.py
Normal file
@ -0,0 +1,18 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||
if not company:
|
||||
return
|
||||
|
||||
custom_fields = {
|
||||
'Sales Invoice Item': [
|
||||
dict(fieldname='taxable_value', label='Taxable Value',
|
||||
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
|
||||
print_hide=1)
|
||||
]
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, update=True)
|
@ -18,6 +18,7 @@ def execute():
|
||||
|
||||
for old_dt, new_dt in doctypes.items():
|
||||
if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt):
|
||||
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt))
|
||||
frappe.rename_doc('DocType', old_dt, new_dt, force=True)
|
||||
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt))
|
||||
frappe.delete_doc_if_exists('DocType', old_dt)
|
||||
@ -36,6 +37,18 @@ def execute():
|
||||
SET parentfield = %(parentfield)s
|
||||
""".format(doctype), {'parentfield': parentfield})
|
||||
|
||||
# copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
|
||||
frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""")
|
||||
|
||||
# rename field
|
||||
frappe.reload_doc('healthcare', 'doctype', 'lab_test')
|
||||
if frappe.db.has_column('Lab Test', 'special_toggle'):
|
||||
|
@ -8,6 +8,7 @@ def execute():
|
||||
|
||||
frappe.reload_doc("healthcare", "doctype", "Therapy Session")
|
||||
frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
|
||||
frappe.reload_doc("healthcare", "doctype", "Clinical Procedure")
|
||||
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
|
||||
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
|
||||
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
|
||||
|
@ -8,6 +8,7 @@
|
||||
"enable",
|
||||
"section_break_2",
|
||||
"sandbox_mode",
|
||||
"applicable_from",
|
||||
"credentials",
|
||||
"auth_token",
|
||||
"token_expiry"
|
||||
@ -48,12 +49,19 @@
|
||||
"fieldname": "sandbox_mode",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sandbox Mode"
|
||||
},
|
||||
{
|
||||
"fieldname": "applicable_from",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Applicable From",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-13 12:04:49.449199",
|
||||
"modified": "2021-03-30 12:26:25.538294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E Invoice Settings",
|
||||
|
@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"gstin",
|
||||
"username",
|
||||
"password"
|
||||
@ -30,12 +31,20 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Password",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-22 15:10:53.466205",
|
||||
"modified": "2021-03-22 12:16:56.365616",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E Invoice User",
|
||||
|
@ -1,12 +1,13 @@
|
||||
erpnext.setup_einvoice_actions = (doctype) => {
|
||||
frappe.ui.form.on(doctype, {
|
||||
async refresh(frm) {
|
||||
const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
|
||||
const supply_type = frm.doc.gst_category;
|
||||
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
|
||||
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
|
||||
const res = await frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
|
||||
args: { doc: frm.doc }
|
||||
});
|
||||
const invoice_eligible = res.message;
|
||||
|
||||
if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
|
||||
if (!invoice_eligible) return;
|
||||
|
||||
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
|
||||
|
||||
@ -109,45 +110,25 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
}
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
const action = () => {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Cancel E-Way Bill'),
|
||||
fields: fields,
|
||||
let message = __('Cancellation of e-way bill is currently not supported. ');
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
|
||||
frappe.msgprint({
|
||||
title: __('Update E-Way Bill Cancelled Status?'),
|
||||
message: message,
|
||||
indicator: 'orange',
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: {
|
||||
doctype,
|
||||
docname: name,
|
||||
eway_bill: ewaybill,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() || d.hide(),
|
||||
error: () => d.hide()
|
||||
callback: () => frm.reload_doc()
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
}
|
||||
|
@ -15,18 +15,43 @@ import traceback
|
||||
import io
|
||||
from frappe import _, bold
|
||||
from pyqrcode import create as qrcreate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.integrations.utils import make_post_request, make_get_request
|
||||
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
|
||||
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
|
||||
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_eligibility(doc):
|
||||
if isinstance(doc, six.string_types):
|
||||
doc = json.loads(doc)
|
||||
|
||||
invalid_doctype = doc.get('doctype') != 'Sales Invoice'
|
||||
if invalid_doctype:
|
||||
return False
|
||||
|
||||
einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
|
||||
if not einvoicing_enabled:
|
||||
return False
|
||||
|
||||
einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01'
|
||||
if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from):
|
||||
return False
|
||||
|
||||
def validate_einvoice_fields(doc):
|
||||
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
||||
invalid_doctype = doc.doctype != 'Sales Invoice'
|
||||
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
no_taxes_applied = not doc.get('taxes')
|
||||
|
||||
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
if invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def validate_einvoice_fields(doc):
|
||||
invoice_eligible = validate_eligibility(doc)
|
||||
|
||||
if not invoice_eligible:
|
||||
return
|
||||
|
||||
if doc.docstatus == 0 and doc._action == 'save':
|
||||
@ -35,6 +60,8 @@ def validate_einvoice_fields(doc):
|
||||
if len(doc.name) > 16:
|
||||
raise_document_name_too_long_error()
|
||||
|
||||
doc.einvoice_status = 'Pending'
|
||||
|
||||
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
|
||||
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
|
||||
|
||||
@ -76,6 +103,9 @@ def get_transaction_details(invoice):
|
||||
))
|
||||
|
||||
def get_doc_details(invoice):
|
||||
if getdate(invoice.posting_date) < getdate('2021-01-01'):
|
||||
frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed'))
|
||||
|
||||
invoice_type = 'CRN' if invoice.is_return else 'INV'
|
||||
|
||||
invoice_name = invoice.name
|
||||
@ -87,56 +117,39 @@ def get_doc_details(invoice):
|
||||
invoice_date=invoice_date
|
||||
))
|
||||
|
||||
def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
|
||||
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
||||
|
||||
if ((not d.gstin and not shipping_address)
|
||||
or not d.city
|
||||
or not d.pincode
|
||||
or not d.address_title
|
||||
or not d.address_line1
|
||||
or not d.gst_state_number):
|
||||
def validate_address_fields(address, is_shipping_address):
|
||||
if ((not address.gstin and not is_shipping_address)
|
||||
or not address.city
|
||||
or not address.pincode
|
||||
or not address.address_title
|
||||
or not address.address_line1
|
||||
or not address.gst_state_number):
|
||||
|
||||
frappe.throw(
|
||||
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
|
||||
get_link_to_form('Address', address_name)
|
||||
),
|
||||
msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
|
||||
title=_('Missing Address Fields')
|
||||
)
|
||||
|
||||
if d.gst_state_number == 97:
|
||||
def get_party_details(address_name, is_shipping_address=False):
|
||||
addr = frappe.get_doc('Address', address_name)
|
||||
|
||||
validate_address_fields(addr, is_shipping_address)
|
||||
|
||||
if addr.gst_state_number == 97:
|
||||
# according to einvoice standard
|
||||
pincode = 999999
|
||||
addr.pincode = 999999
|
||||
|
||||
party_address_details = frappe._dict(dict(
|
||||
legal_name=sanitize_for_json(d.address_title),
|
||||
location=sanitize_for_json(d.city),
|
||||
pincode=d.pincode,
|
||||
state_code=d.gst_state_number,
|
||||
address_line1=sanitize_for_json(d.address_line1),
|
||||
address_line2=sanitize_for_json(d.address_line2)
|
||||
legal_name=sanitize_for_json(addr.address_title),
|
||||
location=sanitize_for_json(addr.city),
|
||||
pincode=addr.pincode, gstin=addr.gstin,
|
||||
state_code=addr.gst_state_number,
|
||||
address_line1=sanitize_for_json(addr.address_line1),
|
||||
address_line2=sanitize_for_json(addr.address_line2)
|
||||
))
|
||||
if d.gstin:
|
||||
party_address_details.gstin = d.gstin
|
||||
|
||||
return party_address_details
|
||||
|
||||
def get_gstin_details(gstin):
|
||||
if not hasattr(frappe.local, 'gstin_cache'):
|
||||
frappe.local.gstin_cache = {}
|
||||
|
||||
key = gstin
|
||||
details = frappe.local.gstin_cache.get(key)
|
||||
if details:
|
||||
return details
|
||||
|
||||
details = frappe.cache().hget('gstin_cache', key)
|
||||
if details:
|
||||
frappe.local.gstin_cache[key] = details
|
||||
return details
|
||||
|
||||
if not details:
|
||||
return GSPConnector.get_gstin_details(gstin)
|
||||
|
||||
def get_overseas_address_details(address_name):
|
||||
address_title, address_line1, address_line2, city = frappe.db.get_value(
|
||||
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
|
||||
@ -171,10 +184,15 @@ def get_item_list(invoice):
|
||||
item.description = sanitize_for_json(d.item_name)
|
||||
|
||||
item.qty = abs(item.qty)
|
||||
item.discount_amount = 0
|
||||
item.unit_rate = abs(item.base_net_amount / item.qty)
|
||||
item.gross_amount = abs(item.base_net_amount)
|
||||
item.taxable_value = abs(item.base_net_amount)
|
||||
|
||||
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||
item.discount_amount = abs(item.base_amount - item.base_net_amount)
|
||||
else:
|
||||
item.discount_amount = 0
|
||||
|
||||
item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
|
||||
item.gross_amount = abs(item.taxable_value) + item.discount_amount
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
|
||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||
@ -207,11 +225,11 @@ def update_item_taxes(invoice, item):
|
||||
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
|
||||
if is_applicable:
|
||||
# this contains item wise tax rate & tax amount (incl. discount)
|
||||
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
|
||||
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
|
||||
|
||||
item_tax_rate = item_tax_detail[0]
|
||||
# item tax amount excluding discount amount
|
||||
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
|
||||
item_tax_amount = (item_tax_rate / 100) * item.taxable_value
|
||||
|
||||
if t.account_head in gst_accounts.cess_account:
|
||||
item_tax_amount_after_discount = item_tax_detail[1]
|
||||
@ -225,6 +243,9 @@ def update_item_taxes(invoice, item):
|
||||
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
||||
item.tax_rate += item_tax_rate
|
||||
item[f'{tax_type}_amount'] += abs(item_tax_amount)
|
||||
else:
|
||||
# TODO: other charges per item
|
||||
pass
|
||||
|
||||
return item
|
||||
|
||||
@ -232,10 +253,14 @@ def get_invoice_value_details(invoice):
|
||||
invoice_value_details = frappe._dict(dict())
|
||||
|
||||
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
||||
invoice_value_details.base_total = abs(invoice.base_total)
|
||||
invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
|
||||
# Discount already applied on net total which means on items
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
invoice_value_details.invoice_discount_amt = 0
|
||||
elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount:
|
||||
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
else:
|
||||
invoice_value_details.base_total = abs(invoice.base_net_total)
|
||||
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
||||
# since tax already considers discount amount
|
||||
invoice_value_details.invoice_discount_amt = 0
|
||||
|
||||
@ -256,7 +281,11 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
||||
invoice_value_details.total_igst_amt = 0
|
||||
invoice_value_details.total_cess_amt = 0
|
||||
invoice_value_details.total_other_charges = 0
|
||||
considered_rows = []
|
||||
|
||||
for t in invoice.taxes:
|
||||
tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \
|
||||
else t.base_tax_amount_after_discount_amount
|
||||
if t.account_head in gst_accounts_list:
|
||||
if t.account_head in gst_accounts.cess_account:
|
||||
# using after discount amt since item also uses after discount amt for cess calc
|
||||
@ -264,12 +293,26 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
||||
|
||||
for tax_type in ['igst', 'cgst', 'sgst']:
|
||||
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
||||
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
|
||||
|
||||
invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
|
||||
update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
|
||||
else:
|
||||
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
|
||||
invoice_value_details.total_other_charges += abs(tax_amount)
|
||||
|
||||
return invoice_value_details
|
||||
|
||||
def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
|
||||
prev_row_id = cint(tax_row.row_id) - 1
|
||||
if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
|
||||
if tax_row.charge_type == 'On Previous Row Amount':
|
||||
amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
|
||||
invoice_value_details.total_other_charges -= abs(amount)
|
||||
considered_rows.append(prev_row_id)
|
||||
if tax_row.charge_type == 'On Previous Row Total':
|
||||
amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
|
||||
invoice_value_details.total_other_charges -= abs(amount)
|
||||
considered_rows.append(prev_row_id)
|
||||
|
||||
def get_payment_details(invoice):
|
||||
payee_name = invoice.company
|
||||
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
||||
@ -282,6 +325,10 @@ def get_payment_details(invoice):
|
||||
))
|
||||
|
||||
def get_return_doc_reference(invoice):
|
||||
if not invoice.return_against:
|
||||
frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
|
||||
.format(frappe.bold('Return Against')), title=_('Missing Field'))
|
||||
|
||||
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
|
||||
return frappe._dict(dict(
|
||||
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
|
||||
@ -289,7 +336,11 @@ def get_return_doc_reference(invoice):
|
||||
|
||||
def get_eway_bill_details(invoice):
|
||||
if invoice.is_return:
|
||||
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
|
||||
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
|
||||
title=_('Invalid Fields'))
|
||||
|
||||
if not invoice.distance:
|
||||
frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
|
||||
|
||||
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
|
||||
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
|
||||
@ -307,9 +358,15 @@ def get_eway_bill_details(invoice):
|
||||
|
||||
def validate_mandatory_fields(invoice):
|
||||
if not invoice.company_address:
|
||||
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
|
||||
frappe.throw(
|
||||
_('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
if not invoice.customer_address:
|
||||
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
|
||||
frappe.throw(
|
||||
_('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
|
||||
frappe.throw(
|
||||
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
||||
@ -321,6 +378,39 @@ def validate_mandatory_fields(invoice):
|
||||
title=_('Missing Fields')
|
||||
)
|
||||
|
||||
def validate_totals(einvoice):
|
||||
item_list = einvoice['ItemList']
|
||||
value_details = einvoice['ValDtls']
|
||||
|
||||
total_item_ass_value = 0
|
||||
total_item_cgst_value = 0
|
||||
total_item_sgst_value = 0
|
||||
total_item_igst_value = 0
|
||||
total_item_value = 0
|
||||
for item in item_list:
|
||||
total_item_ass_value += flt(item['AssAmt'])
|
||||
total_item_cgst_value += flt(item['CgstAmt'])
|
||||
total_item_sgst_value += flt(item['SgstAmt'])
|
||||
total_item_igst_value += flt(item['IgstAmt'])
|
||||
total_item_value += flt(item['TotItemVal'])
|
||||
|
||||
if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
|
||||
frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
|
||||
|
||||
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
|
||||
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
|
||||
|
||||
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - total_item_value) > 1:
|
||||
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
|
||||
|
||||
calculated_invoice_value = \
|
||||
flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
|
||||
+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
|
||||
+ flt(value_details['OthChrg']) - flt(value_details['Discount'])
|
||||
|
||||
if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
|
||||
frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
|
||||
|
||||
def make_einvoice(invoice):
|
||||
validate_mandatory_fields(invoice)
|
||||
|
||||
@ -330,12 +420,12 @@ def make_einvoice(invoice):
|
||||
item_list = get_item_list(invoice)
|
||||
doc_details = get_doc_details(invoice)
|
||||
invoice_value_details = get_invoice_value_details(invoice)
|
||||
seller_details = get_party_details(invoice.company_address, company_address=1)
|
||||
seller_details = get_party_details(invoice.company_address)
|
||||
|
||||
if invoice.gst_category == 'Overseas':
|
||||
buyer_details = get_overseas_address_details(invoice.customer_address)
|
||||
else:
|
||||
buyer_details = get_party_details(invoice.customer_address, billing_address=1)
|
||||
buyer_details = get_party_details(invoice.customer_address)
|
||||
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
|
||||
if place_of_supply:
|
||||
place_of_supply = place_of_supply.split('-')[0]
|
||||
@ -343,20 +433,23 @@ def make_einvoice(invoice):
|
||||
place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
|
||||
buyer_details.update(dict(place_of_supply=place_of_supply))
|
||||
|
||||
seller_details.update(dict(legal_name=invoice.company))
|
||||
buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
|
||||
|
||||
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
|
||||
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
|
||||
if invoice.gst_category == 'Overseas':
|
||||
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
|
||||
else:
|
||||
shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
|
||||
shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True)
|
||||
|
||||
if invoice.is_pos and invoice.base_paid_amount:
|
||||
payment_details = get_payment_details(invoice)
|
||||
|
||||
if invoice.is_return and invoice.return_against:
|
||||
if invoice.is_return:
|
||||
prev_doc_details = get_return_doc_reference(invoice)
|
||||
|
||||
if invoice.transporter:
|
||||
if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
|
||||
eway_bill_details = get_eway_bill_details(invoice)
|
||||
|
||||
# not yet implemented
|
||||
@ -369,18 +462,70 @@ def make_einvoice(invoice):
|
||||
period_details=period_details, prev_doc_details=prev_doc_details,
|
||||
export_details=export_details, eway_bill_details=eway_bill_details
|
||||
)
|
||||
einvoice = safe_json_load(einvoice)
|
||||
|
||||
validations = json.loads(read_json('einv_validation'))
|
||||
errors = validate_einvoice(validations, einvoice)
|
||||
if errors:
|
||||
message = "\n".join([
|
||||
"E Invoice: ", json.dumps(einvoice, indent=4),
|
||||
"-" * 50,
|
||||
"Errors: ", json.dumps(errors, indent=4)
|
||||
])
|
||||
frappe.log_error(title="E Invoice Validation Failed", message=message)
|
||||
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
|
||||
try:
|
||||
einvoice = safe_json_load(einvoice)
|
||||
einvoice = santize_einvoice_fields(einvoice)
|
||||
validate_totals(einvoice)
|
||||
|
||||
except Exception:
|
||||
log_error(einvoice)
|
||||
link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
|
||||
frappe.throw(
|
||||
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
|
||||
invoice.name, link_to_error_list),
|
||||
title=_('E Invoice Creation Failed')
|
||||
)
|
||||
|
||||
return einvoice
|
||||
|
||||
def log_error(data=None):
|
||||
if not isinstance(data, dict):
|
||||
data = json.loads(data)
|
||||
|
||||
seperator = "--" * 50
|
||||
err_tb = traceback.format_exc()
|
||||
err_msg = str(sys.exc_info()[1])
|
||||
data = json.dumps(data, indent=4)
|
||||
|
||||
message = "\n".join([
|
||||
"Error", err_msg, seperator,
|
||||
"Data:", data, seperator,
|
||||
"Exception:", err_tb
|
||||
])
|
||||
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
||||
|
||||
def santize_einvoice_fields(einvoice):
|
||||
int_fields = ["Pin","Distance","CrDay"]
|
||||
float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
|
||||
copy = einvoice.copy()
|
||||
for key, value in copy.items():
|
||||
if isinstance(value, list):
|
||||
for idx, d in enumerate(value):
|
||||
santized_dict = santize_einvoice_fields(d)
|
||||
if santized_dict:
|
||||
einvoice[key][idx] = santized_dict
|
||||
else:
|
||||
einvoice[key].pop(idx)
|
||||
|
||||
if not einvoice[key]:
|
||||
einvoice.pop(key, None)
|
||||
|
||||
elif isinstance(value, dict):
|
||||
santized_dict = santize_einvoice_fields(value)
|
||||
if santized_dict:
|
||||
einvoice[key] = santized_dict
|
||||
else:
|
||||
einvoice.pop(key, None)
|
||||
|
||||
elif not value or value == "None":
|
||||
einvoice.pop(key, None)
|
||||
|
||||
elif key in float_fields:
|
||||
einvoice[key] = flt(value, 2)
|
||||
|
||||
elif key in int_fields:
|
||||
einvoice[key] = cint(value)
|
||||
|
||||
return einvoice
|
||||
|
||||
@ -396,72 +541,22 @@ def safe_json_load(json_string):
|
||||
snippet = json_string[start:end]
|
||||
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
|
||||
|
||||
def validate_einvoice(validations, einvoice, errors=None):
|
||||
if errors is None:
|
||||
errors = []
|
||||
for fieldname, field_validation in validations.items():
|
||||
value = einvoice.get(fieldname, None)
|
||||
if not value or value == "None":
|
||||
# remove keys with empty values
|
||||
einvoice.pop(fieldname, None)
|
||||
continue
|
||||
|
||||
value_type = field_validation.get("type").lower()
|
||||
if value_type in ['object', 'array']:
|
||||
child_validations = field_validation.get('properties')
|
||||
|
||||
if isinstance(value, list):
|
||||
for d in value:
|
||||
validate_einvoice(child_validations, d, errors)
|
||||
if not d:
|
||||
# remove empty dicts
|
||||
einvoice.pop(fieldname, None)
|
||||
else:
|
||||
validate_einvoice(child_validations, value, errors)
|
||||
if not value:
|
||||
# remove empty dicts
|
||||
einvoice.pop(fieldname, None)
|
||||
continue
|
||||
|
||||
# convert to int or str
|
||||
if value_type == 'string':
|
||||
einvoice[fieldname] = str(value)
|
||||
elif value_type == 'number':
|
||||
is_integer = '.' not in str(field_validation.get('maximum'))
|
||||
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
|
||||
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
|
||||
value = einvoice[fieldname]
|
||||
|
||||
max_length = field_validation.get('maxLength')
|
||||
minimum = flt(field_validation.get('minimum'))
|
||||
maximum = flt(field_validation.get('maximum'))
|
||||
pattern_str = field_validation.get('pattern')
|
||||
pattern = re.compile(pattern_str or '')
|
||||
|
||||
label = field_validation.get('description') or fieldname
|
||||
|
||||
if value_type == 'string' and len(value) > max_length:
|
||||
errors.append(_('{} should not exceed {} characters').format(label, max_length))
|
||||
if value_type == 'number' and (value > maximum or value < minimum):
|
||||
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
|
||||
if pattern_str and not pattern.match(value):
|
||||
errors.append(field_validation.get('validationMsg'))
|
||||
|
||||
return errors
|
||||
|
||||
class RequestFailed(Exception): pass
|
||||
class RequestFailed(Exception):
|
||||
pass
|
||||
class CancellationNotAllowed(Exception):
|
||||
pass
|
||||
|
||||
class GSPConnector():
|
||||
def __init__(self, doctype=None, docname=None):
|
||||
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
|
||||
sandbox_mode = self.e_invoice_settings.sandbox_mode
|
||||
self.doctype = doctype
|
||||
self.docname = docname
|
||||
|
||||
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
|
||||
self.credentials = self.get_credentials()
|
||||
self.set_invoice()
|
||||
self.set_credentials()
|
||||
|
||||
# authenticate url is same for sandbox & live
|
||||
self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
|
||||
self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
|
||||
self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test'
|
||||
|
||||
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
|
||||
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
|
||||
@ -470,15 +565,26 @@ class GSPConnector():
|
||||
self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
|
||||
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
|
||||
|
||||
def get_credentials(self):
|
||||
def set_invoice(self):
|
||||
self.invoice = None
|
||||
if self.doctype and self.docname:
|
||||
self.invoice = frappe.get_cached_doc(self.doctype, self.docname)
|
||||
|
||||
def set_credentials(self):
|
||||
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
|
||||
|
||||
if not self.e_invoice_settings.enable:
|
||||
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
|
||||
|
||||
if self.invoice:
|
||||
gstin = self.get_seller_gstin()
|
||||
if not self.e_invoice_settings.enable:
|
||||
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
|
||||
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
|
||||
credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
|
||||
if credentials_for_gstin:
|
||||
self.credentials = credentials_for_gstin[0]
|
||||
else:
|
||||
frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
|
||||
else:
|
||||
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||
return credentials
|
||||
self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||
|
||||
def get_seller_gstin(self):
|
||||
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
||||
@ -529,7 +635,7 @@ class GSPConnector():
|
||||
self.e_invoice_settings.reload()
|
||||
|
||||
except Exception:
|
||||
self.log_error(res)
|
||||
log_error(res)
|
||||
self.raise_error(True)
|
||||
|
||||
def get_headers(self):
|
||||
@ -551,14 +657,14 @@ class GSPConnector():
|
||||
if res.get('success'):
|
||||
return res.get('result')
|
||||
else:
|
||||
self.log_error(res)
|
||||
log_error(res)
|
||||
raise RequestFailed
|
||||
|
||||
except RequestFailed:
|
||||
self.raise_error()
|
||||
|
||||
except Exception:
|
||||
self.log_error()
|
||||
log_error()
|
||||
self.raise_error(True)
|
||||
|
||||
@staticmethod
|
||||
@ -576,12 +682,13 @@ class GSPConnector():
|
||||
return details
|
||||
|
||||
def generate_irn(self):
|
||||
headers = self.get_headers()
|
||||
einvoice = make_einvoice(self.invoice)
|
||||
data = json.dumps(einvoice, indent=4)
|
||||
|
||||
data = {}
|
||||
try:
|
||||
headers = self.get_headers()
|
||||
einvoice = make_einvoice(self.invoice)
|
||||
data = json.dumps(einvoice, indent=4)
|
||||
res = self.make_request('post', self.generate_irn_url, headers, data)
|
||||
|
||||
if res.get('success'):
|
||||
self.set_einvoice_data(res.get('result'))
|
||||
|
||||
@ -601,12 +708,36 @@ class GSPConnector():
|
||||
|
||||
except RequestFailed:
|
||||
errors = self.sanitize_error_message(res.get('message'))
|
||||
self.set_failed_status(errors=errors)
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
self.log_error(data)
|
||||
except Exception as e:
|
||||
self.set_failed_status(errors=str(e))
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
@staticmethod
|
||||
def bulk_generate_irn(invoices):
|
||||
gsp_connector = GSPConnector()
|
||||
gsp_connector.doctype = 'Sales Invoice'
|
||||
|
||||
failed = []
|
||||
|
||||
for invoice in invoices:
|
||||
try:
|
||||
gsp_connector.docname = invoice
|
||||
gsp_connector.set_invoice()
|
||||
gsp_connector.set_credentials()
|
||||
gsp_connector.generate_irn()
|
||||
|
||||
except Exception as e:
|
||||
failed.append({
|
||||
'docname': invoice,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
return failed
|
||||
|
||||
def get_irn_details(self, irn):
|
||||
headers = self.get_headers()
|
||||
|
||||
@ -623,21 +754,30 @@ class GSPConnector():
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
self.log_error()
|
||||
log_error()
|
||||
self.raise_error(True)
|
||||
|
||||
def cancel_irn(self, irn, reason, remark):
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({
|
||||
'Irn': irn,
|
||||
'Cnlrsn': reason,
|
||||
'Cnlrem': remark
|
||||
}, indent=4)
|
||||
|
||||
data, res = {}, {}
|
||||
try:
|
||||
# validate cancellation
|
||||
if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24:
|
||||
frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
|
||||
if not irn:
|
||||
frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
|
||||
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({
|
||||
'Irn': irn,
|
||||
'Cnlrsn': reason,
|
||||
'Cnlrem': remark
|
||||
}, indent=4)
|
||||
|
||||
res = self.make_request('post', self.cancel_irn_url, headers, data)
|
||||
if res.get('success'):
|
||||
if res.get('success') or '9999' in res.get('message'):
|
||||
self.invoice.irn_cancelled = 1
|
||||
self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else ""
|
||||
self.invoice.einvoice_status = 'Cancelled'
|
||||
self.invoice.flags.updater_reference = {
|
||||
'doctype': self.invoice.doctype,
|
||||
'docname': self.invoice.name,
|
||||
@ -650,12 +790,41 @@ class GSPConnector():
|
||||
|
||||
except RequestFailed:
|
||||
errors = self.sanitize_error_message(res.get('message'))
|
||||
self.set_failed_status(errors=errors)
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
self.log_error(data)
|
||||
except CancellationNotAllowed as e:
|
||||
self.set_failed_status(errors=str(e))
|
||||
self.raise_error(errors=str(e))
|
||||
|
||||
except Exception as e:
|
||||
self.set_failed_status(errors=str(e))
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
@staticmethod
|
||||
def bulk_cancel_irn(invoices, reason, remark):
|
||||
gsp_connector = GSPConnector()
|
||||
gsp_connector.doctype = 'Sales Invoice'
|
||||
|
||||
failed = []
|
||||
|
||||
for invoice in invoices:
|
||||
try:
|
||||
gsp_connector.docname = invoice
|
||||
gsp_connector.set_invoice()
|
||||
gsp_connector.set_credentials()
|
||||
irn = gsp_connector.invoice.irn
|
||||
gsp_connector.cancel_irn(irn, reason, remark)
|
||||
|
||||
except Exception as e:
|
||||
failed.append({
|
||||
'docname': invoice,
|
||||
'message': str(e)
|
||||
})
|
||||
|
||||
return failed
|
||||
|
||||
def generate_eway_bill(self, **kwargs):
|
||||
args = frappe._dict(kwargs)
|
||||
|
||||
@ -694,7 +863,7 @@ class GSPConnector():
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
self.log_error(data)
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
def cancel_eway_bill(self, eway_bill, reason, remark):
|
||||
@ -726,7 +895,7 @@ class GSPConnector():
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
self.log_error(data)
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
def sanitize_error_message(self, message):
|
||||
@ -741,6 +910,9 @@ class GSPConnector():
|
||||
]
|
||||
then we trim down the message by looping over errors
|
||||
'''
|
||||
if not message:
|
||||
return []
|
||||
|
||||
errors = re.findall(': [^:]+', message)
|
||||
for idx, e in enumerate(errors):
|
||||
# remove colons
|
||||
@ -752,22 +924,6 @@ class GSPConnector():
|
||||
|
||||
return errors
|
||||
|
||||
def log_error(self, data={}):
|
||||
if not isinstance(data, dict):
|
||||
data = json.loads(data)
|
||||
|
||||
seperator = "--" * 50
|
||||
err_tb = traceback.format_exc()
|
||||
err_msg = str(sys.exc_info()[1])
|
||||
data = json.dumps(data, indent=4)
|
||||
|
||||
message = "\n".join([
|
||||
"Error", err_msg, seperator,
|
||||
"Data:", data, seperator,
|
||||
"Exception:", err_tb
|
||||
])
|
||||
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
||||
|
||||
def raise_error(self, raise_exception=False, errors=[]):
|
||||
title = _('E Invoice Request Failed')
|
||||
if errors:
|
||||
@ -790,7 +946,10 @@ class GSPConnector():
|
||||
self.invoice.ack_no = res.get('AckNo')
|
||||
self.invoice.ack_date = res.get('AckDt')
|
||||
self.invoice.signed_einvoice = dec_signed_invoice
|
||||
self.invoice.ack_no = res.get('AckNo')
|
||||
self.invoice.ack_date = res.get('AckDt')
|
||||
self.invoice.signed_qr_code = res.get('SignedQRCode')
|
||||
self.invoice.einvoice_status = 'Generated'
|
||||
|
||||
self.attach_qrcode_image()
|
||||
|
||||
@ -827,6 +986,17 @@ class GSPConnector():
|
||||
self.invoice.flags.ignore_validate = True
|
||||
self.invoice.save()
|
||||
|
||||
def set_failed_status(self, errors=None):
|
||||
frappe.db.rollback()
|
||||
self.invoice.einvoice_status = 'Failed'
|
||||
self.invoice.failure_description = self.get_failure_message(errors) if errors else ""
|
||||
self.update_invoice()
|
||||
frappe.db.commit()
|
||||
|
||||
def get_failure_message(self, errors):
|
||||
if isinstance(errors, list):
|
||||
errors = ', '.join(errors)
|
||||
return errors
|
||||
|
||||
def sanitize_for_json(string):
|
||||
"""Escape JSON specific characters from a string."""
|
||||
@ -856,5 +1026,114 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
# TODO: uncomment when eway_bill api from Adequare is enabled
|
||||
# gsp_connector = GSPConnector(doctype, docname)
|
||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
|
||||
# update cancelled status only, to be able to cancel irn next
|
||||
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_einvoices(docnames):
|
||||
docnames = json.loads(docnames) or []
|
||||
|
||||
if len(docnames) < 10:
|
||||
failures = GSPConnector.bulk_generate_irn(docnames)
|
||||
frappe.local.message_log = []
|
||||
|
||||
if failures:
|
||||
show_bulk_action_failure_message(failures)
|
||||
|
||||
success = len(docnames) - len(failures)
|
||||
frappe.msgprint(
|
||||
_('{} e-invoices generated successfully').format(success),
|
||||
title=_('Bulk E-Invoice Generation Complete')
|
||||
)
|
||||
|
||||
else:
|
||||
enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames)
|
||||
|
||||
def schedule_bulk_generate_irn(docnames):
|
||||
failures = GSPConnector.bulk_generate_irn(docnames)
|
||||
frappe.local.message_log = []
|
||||
|
||||
frappe.publish_realtime("bulk_einvoice_generation_complete", {
|
||||
"user": frappe.session.user,
|
||||
"failures": failures,
|
||||
"invoices": docnames
|
||||
})
|
||||
|
||||
def show_bulk_action_failure_message(failures):
|
||||
for doc in failures:
|
||||
docname = '<a href="sales-invoice/{0}">{0}</a>'.format(doc.get('docname'))
|
||||
message = doc.get('message').replace("'", '"')
|
||||
if message[0] == '[':
|
||||
errors = json.loads(message)
|
||||
error_list = ''.join(['<li>{}</li>'.format(err) for err in errors])
|
||||
message = '''{} has following errors:<br>
|
||||
<ul style="padding-left: 20px; padding-top: 5px">{}</ul>'''.format(docname, error_list)
|
||||
else:
|
||||
message = '{} - {}'.format(docname, message)
|
||||
|
||||
frappe.msgprint(
|
||||
message,
|
||||
title=_('Bulk E-Invoice Generation Complete'),
|
||||
indicator='red'
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_irns(docnames, reason, remark):
|
||||
docnames = json.loads(docnames) or []
|
||||
|
||||
if len(docnames) < 10:
|
||||
failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
|
||||
frappe.local.message_log = []
|
||||
|
||||
if failures:
|
||||
show_bulk_action_failure_message(failures)
|
||||
|
||||
success = len(docnames) - len(failures)
|
||||
frappe.msgprint(
|
||||
_('{} e-invoices cancelled successfully').format(success),
|
||||
title=_('Bulk E-Invoice Cancellation Complete')
|
||||
)
|
||||
else:
|
||||
enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark)
|
||||
|
||||
def schedule_bulk_cancel_irn(docnames, reason, remark):
|
||||
failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
|
||||
frappe.local.message_log = []
|
||||
|
||||
frappe.publish_realtime("bulk_einvoice_cancellation_complete", {
|
||||
"user": frappe.session.user,
|
||||
"failures": failures,
|
||||
"invoices": docnames
|
||||
})
|
||||
|
||||
def enqueue_bulk_action(job, **kwargs):
|
||||
check_scheduler_status()
|
||||
|
||||
enqueue(
|
||||
job,
|
||||
**kwargs,
|
||||
queue="long",
|
||||
timeout=10000,
|
||||
event="processing_bulk_einvoice_action",
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
)
|
||||
|
||||
if job == schedule_bulk_generate_irn:
|
||||
msg = _('E-Invoices will be generated in a background process.')
|
||||
else:
|
||||
msg = _('E-Invoices will be cancelled in a background process.')
|
||||
|
||||
frappe.msgprint(msg, alert=1)
|
||||
|
||||
def check_scheduler_status():
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
|
||||
|
||||
def job_already_enqueued(job_name):
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
if job_name in enqueued_jobs:
|
||||
return True
|
@ -51,7 +51,7 @@ def create_hsn_codes(data, code_field):
|
||||
|
||||
def add_custom_roles_for_reports():
|
||||
for report_name in ('GST Sales Register', 'GST Purchase Register',
|
||||
'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'):
|
||||
'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'):
|
||||
|
||||
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
|
||||
frappe.get_doc(dict(
|
||||
@ -127,6 +127,9 @@ def make_custom_fields(update=True):
|
||||
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
|
||||
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
|
||||
print_hide=1)
|
||||
taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
|
||||
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
|
||||
print_hide=1)
|
||||
|
||||
purchase_invoice_gst_category = [
|
||||
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
|
||||
@ -415,21 +418,37 @@ def make_custom_fields(update=True):
|
||||
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
|
||||
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
|
||||
|
||||
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||
|
||||
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||
|
||||
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
|
||||
print_hide=1, hidden=1),
|
||||
|
||||
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
|
||||
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
|
||||
no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
|
||||
options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||
|
||||
dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
|
||||
hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
|
||||
]
|
||||
|
||||
custom_fields = {
|
||||
@ -460,7 +479,7 @@ def make_custom_fields(update=True):
|
||||
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
|
||||
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
import frappe, re, json
|
||||
from frappe import _
|
||||
import erpnext
|
||||
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
|
||||
from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
|
||||
from erpnext.regional.india import states, state_numbers
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
@ -41,24 +41,25 @@ def validate_gstin_for_india(doc, method):
|
||||
return
|
||||
|
||||
if len(doc.gstin) != 15:
|
||||
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
|
||||
frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN"))
|
||||
|
||||
if gst_category and gst_category == 'UIN Holders':
|
||||
if not GSTIN_UIN_FORMAT.match(doc.gstin):
|
||||
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
|
||||
frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"),
|
||||
title=_("Invalid GSTIN"))
|
||||
else:
|
||||
if not GSTIN_FORMAT.match(doc.gstin):
|
||||
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
|
||||
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
|
||||
|
||||
validate_gstin_check_digit(doc.gstin)
|
||||
set_gst_state_and_state_number(doc)
|
||||
|
||||
if not doc.gst_state:
|
||||
frappe.throw(_("Please Enter GST state"))
|
||||
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
|
||||
|
||||
if doc.gst_state_number != doc.gstin[:2]:
|
||||
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
|
||||
.format(doc.gst_state_number))
|
||||
frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.")
|
||||
.format(doc.gst_state_number), title=_("Invalid GSTIN"))
|
||||
|
||||
def validate_pan_for_india(doc, method):
|
||||
if doc.get('country') != 'India' or not doc.pan:
|
||||
@ -832,3 +833,48 @@ def get_regional_round_off_accounts(company, account_list):
|
||||
account_list.extend(gst_account_list)
|
||||
|
||||
return account_list
|
||||
|
||||
def update_taxable_values(doc, method):
|
||||
country = frappe.get_cached_value('Company', doc.company, 'country')
|
||||
|
||||
if country != 'India':
|
||||
return
|
||||
|
||||
gst_accounts = get_gst_accounts(doc.company)
|
||||
|
||||
# Only considering sgst account to avoid inflating taxable value
|
||||
gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \
|
||||
+ gst_accounts.get('igst_account', [])
|
||||
|
||||
additional_taxes = 0
|
||||
total_charges = 0
|
||||
item_count = 0
|
||||
considered_rows = []
|
||||
|
||||
for tax in doc.get('taxes'):
|
||||
prev_row_id = cint(tax.row_id) - 1
|
||||
if tax.account_head in gst_account_list and prev_row_id not in considered_rows:
|
||||
if tax.charge_type == 'On Previous Row Amount':
|
||||
additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount
|
||||
considered_rows.append(prev_row_id)
|
||||
if tax.charge_type == 'On Previous Row Total':
|
||||
additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total
|
||||
considered_rows.append(prev_row_id)
|
||||
|
||||
for item in doc.get('items'):
|
||||
if doc.apply_discount_on == 'Grand Total' and doc.discount_amount:
|
||||
proportionate_value = item.base_amount if doc.base_total else item.qty
|
||||
total_value = doc.base_total if doc.base_total else doc.total_qty
|
||||
else:
|
||||
proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
|
||||
total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
|
||||
|
||||
applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)),
|
||||
item.precision('taxable_value')))
|
||||
item.taxable_value = applicable_charges + proportionate_value
|
||||
total_charges += applicable_charges
|
||||
item_count += 1
|
||||
|
||||
if total_charges != additional_taxes:
|
||||
diff = additional_taxes - total_charges
|
||||
doc.get('items')[item_count - 1].taxable_value += diff
|
||||
|
@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["E-Invoice Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
"fieldname": "customer",
|
||||
"label": __("Customer")
|
||||
},
|
||||
{
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"fieldname": "to_date",
|
||||
"label": __("To Date"),
|
||||
"default": frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
"fieldtype": "Select",
|
||||
"fieldname": "status",
|
||||
"label": __("Status"),
|
||||
"options": "\nPending\nGenerated\nCancelled\nFailed"
|
||||
}
|
||||
],
|
||||
|
||||
"formatter": function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname == "einvoice_status" && value) {
|
||||
if (value == 'Pending') value = `<span class="bold" style="color: var(--text-on-orange)">${value}</span>`;
|
||||
else if (value == 'Generated') value = `<span class="bold" style="color: var(--text-on-green)">${value}</span>`;
|
||||
else if (value == 'Cancelled') value = `<span class="bold" style="color: var(--text-on-red)">${value}</span>`;
|
||||
else if (value == 'Failed') value = `<span class="bold" style="color: var(--text-on-red)">${value}</span>`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-03-12 11:23:37.312294",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{}",
|
||||
"letter_head": "Logo",
|
||||
"modified": "2021-03-12 12:36:48.689413",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E-Invoice Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "E-Invoice Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Administrator"
|
||||
}
|
||||
]
|
||||
}
|
106
erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
Normal file
106
erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
Normal file
@ -0,0 +1,106 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def validate_filters(filters={}):
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
if not filters.company:
|
||||
frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter'))
|
||||
if filters.company:
|
||||
# validate if company has e-invoicing enabled
|
||||
pass
|
||||
if not filters.from_date or not filters.to_date:
|
||||
frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter'))
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter'))
|
||||
|
||||
def get_data(filters={}):
|
||||
query_filters = {
|
||||
'posting_date': ['between', [filters.from_date, filters.to_date]],
|
||||
'einvoice_status': ['is', 'set'],
|
||||
'company': filters.company
|
||||
}
|
||||
if filters.customer:
|
||||
query_filters['customer'] = filters.customer
|
||||
if filters.status:
|
||||
query_filters['einvoice_status'] = filters.status
|
||||
|
||||
data = frappe.get_all(
|
||||
'Sales Invoice',
|
||||
filters=query_filters,
|
||||
fields=[d.get('fieldname') for d in get_columns()]
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "posting_date",
|
||||
"label": _("Posting Date"),
|
||||
"width": 0
|
||||
},
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "name",
|
||||
"label": _("Sales Invoice"),
|
||||
"options": "Sales Invoice",
|
||||
"width": 140
|
||||
},
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "einvoice_status",
|
||||
"label": _("Status"),
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "customer",
|
||||
"options": "Customer",
|
||||
"label": _("Customer")
|
||||
},
|
||||
{
|
||||
"fieldtype": "Check",
|
||||
"fieldname": "is_return",
|
||||
"label": _("Is Return"),
|
||||
"width": 85
|
||||
},
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "ack_no",
|
||||
"label": "Ack. No.",
|
||||
"width": 145
|
||||
},
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "ack_date",
|
||||
"label": "Ack. Date",
|
||||
"width": 165
|
||||
},
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "irn",
|
||||
"label": _("IRN No."),
|
||||
"width": 250
|
||||
},
|
||||
{
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"fieldname": "base_grand_total",
|
||||
"label": _("Grand Total"),
|
||||
"width": 120
|
||||
}
|
||||
]
|
@ -279,11 +279,6 @@ erpnext.PointOfSale.Controller = class {
|
||||
const item_row = frappe.model.get_doc(cdt, cdn);
|
||||
if (item_row && item_row[fieldname] != value) {
|
||||
|
||||
if (fieldname === 'qty' && flt(value) == 0) {
|
||||
this.remove_item_from_cart();
|
||||
return;
|
||||
}
|
||||
|
||||
const { item_code, batch_no, uom } = this.item_details.current_item;
|
||||
const event = {
|
||||
field: fieldname,
|
||||
|
@ -27,7 +27,7 @@ def delete_company_transactions(company_name):
|
||||
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
|
||||
"Party Account", "Employee", "Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
|
||||
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
|
||||
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Mode of Payment Account",
|
||||
"Item Default", "Customer", "Supplier", "GST Account"):
|
||||
delete_for_doctype(doctype, company_name)
|
||||
|
||||
|
@ -59,6 +59,8 @@
|
||||
"show_in_website": 1,
|
||||
"website_warehouse": "_Test Warehouse - _TC",
|
||||
"gst_hsn_code": "999800",
|
||||
"opening_stock": 10,
|
||||
"valuation_rate": 100,
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
|
@ -379,7 +379,6 @@ def create_stock_entry(pick_list):
|
||||
else:
|
||||
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
||||
|
||||
stock_entry.set_incoming_rate()
|
||||
stock_entry.set_actual_qty()
|
||||
stock_entry.calculate_rate_and_amount()
|
||||
|
||||
|
@ -127,7 +127,7 @@ def repost_entries():
|
||||
check_if_stock_and_account_balance_synced(today(), d.name)
|
||||
|
||||
def get_repost_item_valuation_entries():
|
||||
date = add_to_date(today(), hours=-12)
|
||||
date = add_to_date(today(), hours=-3)
|
||||
|
||||
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
|
||||
WHERE status != 'Completed' and creation <= %s and docstatus = 1
|
||||
|
@ -70,6 +70,7 @@
|
||||
"oldfieldname": "company",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Company",
|
||||
"read_only_depends_on": "eval: !doc.__islocal",
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
@ -244,7 +245,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-16 17:21:52.380098",
|
||||
"modified": "2021-04-09 19:54:56.263965",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
|
Loading…
Reference in New Issue
Block a user