Merge branch 'develop' into dimension-wise-accounts-balance-reports

This commit is contained in:
Afshan 2021-04-13 19:38:04 +05:30 committed by GitHub
commit 82ebc47ba1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 2249 additions and 722 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.0-dev' __version__ = '13.0.1'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) {
}); });
}); });
frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field", frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
frm.doc.name).options = options; 'bank_transaction_field', 'options', options
);
frm.fields_dict.bank_transaction_mapping.grid.refresh();
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {

View File

@ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
}, },
setup_balance_formatter: function() { setup_balance_formatter: function() {
var me = this; const formatter = function(value, df, options, doc) {
$.each(["balance", "party_balance"], function(i, field) { var currency = frappe.meta.get_field_currency(df, doc);
var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name); var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
df.formatter = function(value, df, options, doc) { return "<div style='text-align: right'>"
var currency = frappe.meta.get_field_currency(df, doc); + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : ""; + " " + dr_or_cr
return "<div style='text-align: right'>" + "</div>";
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) };
+ " " + dr_or_cr this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
+ "</div>"; this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
}
})
}, },
reference_name: function(doc, cdt, cdn) { reference_name: function(doc, cdt, cdn) {
@ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) {
cur_frm.cscript.update_totals(doc); cur_frm.cscript.update_totals(doc);
} }
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Journal Entry");
}
frappe.ui.form.on("Journal Entry Account", { frappe.ui.form.on("Journal Entry Account", {
party: function(frm, cdt, cdn) { party: function(frm, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn); var d = frappe.get_doc(cdt, cdn);
@ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, {
}; };
$.each(field_label_map, function (fieldname, label) { $.each(field_label_map, function (fieldname, label) {
var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name); frm.fields_dict.accounts.grid.update_docfield_property(
df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label; fieldname,
'label',
frm.doc.multi_currency ? (label + " in Account Currency") : label
);
}) })
}, },

View File

@ -280,7 +280,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-06-24 14:06:54.833738", "modified": "2020-06-26 14:06:54.833738",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}); });
if (invoices) { if (invoices) {
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number", this.frm.fields_dict.payment.grid.update_docfield_property(
me.frm.doc.name).options = "\n" + invoices.join("\n"); 'invoice_number', 'options', "\n" + invoices.join("\n")
);
$.each(me.frm.doc.payments || [], function(i, p) { $.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;

View File

@ -108,7 +108,6 @@ class POSInvoice(SalesInvoice):
filters = { "item_code": d.item_code, "warehouse": d.warehouse } filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no: if d.batch_no:
filters["batch_no"] = d.batch_no filters["batch_no"] = d.batch_no
reserved_serial_nos = get_pos_reserved_serial_nos(filters) reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no) serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]

View File

@ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
import json
from six import iteritems from six import iteritems
@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1 sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save() sales_invoice.save()
sales_invoice.submit() sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name self.consolidated_invoice = sales_invoice.name
return 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 = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1 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? # TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
return 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: 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.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) 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 found = True
if not found: if not found:
tax.charge_type = 'Actual' tax.charge_type = 'Actual'
tax.included_in_print_rate = 0 tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_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) taxes.append(tax)
for payment in doc.get('payments'): for payment in doc.get('payments'):
@ -168,8 +177,6 @@ class POSInvoiceMergeLog(Document):
sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer sales_invoice.customer = self.customer
sales_invoice.is_pos = 1 sales_invoice.is_pos = 1
# date can be pos closing date?
sales_invoice.posting_date = getdate(nowdate())
return sales_invoice return sales_invoice
@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
si.flags.ignore_validate = True si.flags.ignore_validate = True
si.cancel() 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(): def get_all_unconsolidated_invoices():
filters = { filters = {
'consolidated_invoice': [ 'in', [ '', None ]], 'consolidated_invoice': [ 'in', [ '', None ]],
@ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 5 and closing_entry:
closing_entry.set_status(update=True, status='Queued') 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: else:
create_merge_logs(invoice_by_customer, closing_entry) create_merge_logs(invoice_by_customer, closing_entry)
@ -227,14 +254,14 @@ def unconsolidate_pos_invoices(closing_entry):
if len(merge_logs) >= 5: if len(merge_logs) >= 5:
closing_entry.set_status(update=True, status='Queued') 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: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}): def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer): for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log') 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.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None) merge_log.pos_closing_entry = closing_entry.get('name', None)
@ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled') closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True) 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() check_scheduler_status()
job_name = closing_entry.get("name") job_name = closing_entry.get("name")
@ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry):
job_name=job_name, job_name=job_name,
closing_entry=closing_entry, closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer, invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test
) )

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice 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.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices 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 Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") 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`")

View File

@ -70,6 +70,7 @@ class POSProfile(Document):
{"parent": d.mode_of_payment, "company": self.company}, {"parent": d.mode_of_payment, "company": self.company},
"default_account" "default_account"
) )
if not account: if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))

View File

@ -92,11 +92,21 @@ def make_pos_profile(**args):
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
}) })
payments = [{ mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
company = args.company or "_Test Company"
default_account = args.income_account or "Sales - _TC"
if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}):
mode_of_payment.append("accounts", {
"company": company,
"default_account": default_account
})
mode_of_payment.save()
pos_profile.append("payments", {
'mode_of_payment': 'Cash', 'mode_of_payment': 'Cash',
'default': 1 'default': 1
}] })
pos_profile.set("payments", payments)
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert() pos_profile.insert()

View File

@ -16,8 +16,11 @@ frappe.ui.form.on('POS Settings', {
} }
}); });
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields); frm.fields_dict.invoice_fields.grid.update_docfield_property(
'fieldname', 'options', [""].concat(fields)
);
}); });
} }
}); });

View File

@ -328,6 +328,21 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110) self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990) self.assertEquals(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
rate_or_discount="Discount Amount", discount_amount=110)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEquals(item.margin_rate_or_amount, 10)
self.assertEquals(item.rate_with_margin, 1100)
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self): def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = { test_record = {
@ -560,6 +575,7 @@ def make_pricing_rule(**args):
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '', "condition": args.condition or '',
"priority": 1, "priority": 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })

View File

@ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field): continue if not d.get(pr_field): continue
if d.validate_applied_rule and doc.get(field) < d.get(pr_field): if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}") frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name)) .format(doc.name))
else: else:

View File

@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.refresh_field('customers'); frm.refresh_field('customers');
} }
else{ else{
frappe.msgprint('No Customers found with selected options.'); frappe.throw('No Customers found with selected options.');
} }
} }
} }

View File

@ -126,9 +126,11 @@ def get_customers_based_on_sales_person(sales_person):
sales_person_records = frappe._dict() sales_person_records = frappe._dict()
for d in records: for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent) sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ if sales_person_records.get('Customer'):
return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['name', 'in', list(sales_person_records['Customer'])]]) filters=[['name', 'in', list(sales_person_records['Customer'])]])
return customers else:
return []
def get_recipients_and_cc(customer, doc): def get_recipients_and_cc(customer, doc):
recipients = [] recipients = []

View File

@ -496,15 +496,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
} }
} }
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}
frappe.ui.form.on("Purchase Invoice", { frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {

View File

@ -127,7 +127,6 @@
"write_off_cost_center", "write_off_cost_center",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances", "get_advances",
"advances", "advances",
"payment_schedule_section", "payment_schedule_section",
@ -1326,13 +1325,6 @@
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{ {
"depends_on": "eval:doc.is_internal_supplier", "depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
@ -1378,7 +1370,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-09 21:12:30.422084", "modified": "2021-03-30 22:45:58.334107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -1,14 +1,14 @@
var globalOnload = frappe.listview_settings['Sales Invoice'].onload; 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 // Provision in case onload event is added to sales_invoice.js in future
if (globalOnload) { if (globalOnload) {
globalOnload(doclist); globalOnload(list_view);
} }
const action = () => { const action = () => {
const selected_docs = doclist.get_checked_items(); const selected_docs = list_view.get_checked_items();
const docnames = doclist.get_checked_items(true); const docnames = list_view.get_checked_items(true);
for (let doc of selected_docs) { for (let doc of selected_docs) {
if (doc.docstatus !== 1) { if (doc.docstatus !== 1) {
@ -19,7 +19,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
frappe.call({ frappe.call({
method: 'erpnext.regional.india.utils.generate_ewb_json', method: 'erpnext.regional.india.utils.generate_ewb_json',
args: { args: {
'dt': doclist.doctype, 'dt': list_view.doctype,
'dn': docnames 'dn': docnames
}, },
callback: function(r) { 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'
});
}
});
}; };

View File

@ -1,9 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
// print heading
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
@ -916,7 +913,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
}, },
callback: function(r, rt) { callback: function(r, rt) {
if(r.message){ 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_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);

View File

@ -24,6 +24,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.healthcare.utils import manage_invoice_submit_cancel
@ -211,6 +212,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if self.update_stock == 1: if self.update_stock == 1:
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()

View File

@ -1166,10 +1166,12 @@ class TestSalesInvoice(unittest.TestCase):
def test_create_so_with_margin(self): def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = 100 price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = 'Percentage' si.items[0].margin_type = 'Percentage'
si.items[0].margin_rate_or_amount = 25 si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
si.save() si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))

View File

@ -406,9 +406,10 @@ def check_if_advance_entry_modified(args):
throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) throw(_("""Payment Entry has been modified after you pulled it. Please pull it again."""))
def validate_allocated_amount(args): def validate_allocated_amount(args):
precision = args.get('precision') or frappe.db.get_single_value("System Settings", "currency_precision")
if args.get("allocated_amount") < 0: if args.get("allocated_amount") < 0:
throw(_("Allocated amount cannot be negative")) throw(_("Allocated amount cannot be negative"))
elif args.get("allocated_amount") > args.get("unadjusted_amount"): elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision):
throw(_("Allocated amount cannot be greater than unadjusted amount")) throw(_("Allocated amount cannot be greater than unadjusted amount"))
def update_reference_in_journal_entry(d, jv_obj): def update_reference_in_journal_entry(d, jv_obj):

View File

@ -443,6 +443,16 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "UAE VAT 201",
"link_to": "UAE VAT 201",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,

View File

@ -0,0 +1,471 @@
# Version 13.0.0 Release Notes
### Accounting
- [New and refreshed POS](https://github.com/frappe/erpnext/pull/20789)
- [GST E-invoicing for India](https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing)
- [Distributed Cost Center](https://docs.erpnext.com/docs/user/manual/en/accounts/distributed-cost-center)
- [Process Bulk Statement Of Accounts](https://docs.erpnext.com/docs/user/manual/en/accounts/process-statement-of-accounts)
- [More controlled deferred revenue booking](https://docs.erpnext.com/docs/user/manual/en/accounts/process-deferred-accounting)
- [Dunning](https://docs.erpnext.com/docs/user/manual/en/accounts/dunning)
- [Journal Entry Template](https://docs.erpnext.com/docs/user/manual/en/accounts/journal-entry-template)
- [POS Register report](https://github.com/frappe/erpnext/pull/23313)
- [UAE VAT 201 Report](https://github.com/frappe/erpnext/pull/23447)
### Loan Management
- [Loan Application](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-application)
- [Loan](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan)
- [Loan Security Pledge](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-security-pledge)
- [Loan Disbursement](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-disbursement)
- [Loan Repayment](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-repayment)
- [Loan Interest Accrual](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-interest-accrual)
- [Loan Write Off](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-write-off)
### Healthcare
- [Refactored Healthcare Module](https://docs.erpnext.com/docs/user/manual/en/healthcare)
- [Rehabilitation Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/exercise_type)
- [Laboratory Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/setup_laboratory)
- [Patient Progress Page](https://github.com/frappe/erpnext/pull/22474)
- [Inpatient Medication Order and Entry](https://docs.erpnext.com/docs/user/manual/en/healthcare/inpatient_medication_entry)
- [Therapy Plan Template](https://docs.erpnext.com/docs/user/manual/en/healthcare/therapy_plan)
- [Multi company support in Healthcare](https://github.com/frappe/erpnext/pull/21290)
- [Inpatient Medication Orders Script Report](https://github.com/frappe/erpnext/pull/23984)
- [Patient History Enhancements](https://github.com/frappe/erpnext/pull/24033)
### Stock
- [Putaway](https://docs.erpnext.com/docs/user/manual/en/stock/putaway-rule)
- [More accurate stock valuation in case of back-dated stock transactions](https://github.com/frappe/erpnext/pull/24183)
- [Repost item costing via background job](https://github.com/frappe/erpnext/pull/24183)
- [Item valuation for internal stock transfers](https://github.com/frappe/erpnext/pull/24200)
- [Multi currency in Landed Cost Voucher](https://github.com/frappe/erpnext/pull/24127)
- [Formula based Quality Inspection](https://docs.erpnext.com/docs/user/manual/en/stock/quality-inspection)
- [Value Based and Numeric Quality Inspection](https://github.com/frappe/erpnext/pull/24181)
- [Shipment](https://github.com/frappe/erpnext/pull/22914)
- [Return tracking in PR/DN](https://github.com/frappe/erpnext/pull/22859)
### Manufacturing
- [Production forecasting using Exponential Smoothing method](https://docs.erpnext.com/docs/user/manual/en/manufacturing/reports/demand-driven-forecasting)
- [BOM Template](https://docs.erpnext.com/docs/user/manual/en/manufacturing/bill-of-materials#34-bom-template)
- [Downtime Entry](https://docs.erpnext.com/docs/user/manual/en/manufacturing/downtime-entry)
- [Quality Inspection on Job Card](https://github.com/frappe/erpnext/pull/23964)
- New Reports
- Production Planning Report ([#21763](https://github.com/frappe/erpnext/pull/21763))
- BOM Operations Time ([#21763](https://github.com/frappe/erpnext/pull/21763))
- Work Order Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Job card Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Downtime Analysis ([#21430](https://github.com/frappe/erpnext/pull/21430))
- Quality Inspection ([#21430](https://github.com/frappe/erpnext/pull/21430))
### HR
- [Leave policy assignment](https://github.com/frappe/erpnext/pull/23112)
- [In and Out time in attendance](https://github.com/frappe/erpnext/pull/21547)
- [Shift management](https://docs.erpnext.com/docs/user/manual/en/human-resources/shift-management)
- [Recruitment analytics](https://github.com/frappe/erpnext/pull/21732)
- [Bulk Mark Attendance](https://github.com/frappe/erpnext/pull/20062)
- [Leave type with partial payment](https://github.com/frappe/erpnext/pull/23173)
- New and enhanced reports
- Employee Analytics ([#21705](https://github.com/frappe/erpnext/pull/21705))
- Employee Leave Balance ([#20754](https://github.com/frappe/erpnext/pull/20754))
- Employee Leave Balance Summary ([#20754](https://github.com/frappe/erpnext/pull/20754))
### Payroll
- [Multi-currency payroll](https://github.com/frappe/erpnext/pull/23519)
- [Payroll based on attendance](https://github.com/frappe/erpnext/pull/21258)
- [Payroll based on employee cost center](https://github.com/frappe/erpnext/pull/21609)
- [Recurring Additional Salary](https://github.com/frappe/erpnext/pull/20936)
- [Compute Year to Date for Salary Slip components](https://github.com/frappe/erpnext/pull/24362)
- New Reports
- Income Tax Deductions
- Professional Tax Deductions
- Provident Fund Deductions
- Total Salary Payments Based on Payment Mode
- Salary Payments via ECS
### CRM
- [Social Media Post](https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post)
- [Make Quotation against Blanket Order](https://docs.erpnext.com/docs/user/manual/en/selling/blanket-order)
- [Calendar View for Opportunity](https://github.com/frappe/erpnext/pull/21280)
### Selling
- [Batch wise item pricing](https://github.com/frappe/erpnext/pull/24470)
- [Refreshed shopping cart](https://github.com/frappe/erpnext/pull/22617)
- [Territory-wise Sales Report](https://github.com/frappe/erpnext/pull/20428)
#### Buying
- [Multi UOM support in Request for Quotation](https://github.com/frappe/erpnext/pull/22249)
- [Provision to make RFQ against Opportunity](https://github.com/frappe/erpnext/pull/22765)
- [Item Rate in Stock UOM in purchase cycle](https://github.com/frappe/erpnext/pull/24315)
- New Reports
- Requested Items To Order ([#21611](https://github.com/frappe/erpnext/pull/21611))
- Purchase Order Analysis ([#21611](https://github.com/frappe/erpnext/pull/21611))
- Supplier Quotation Comparison report ([#23323](https://github.com/frappe/erpnext/pull/23323))
### Project
- [Project template with dependent tasks](https://github.com/frappe/erpnext/pull/24092)
- [Project Summary Report](https://github.com/frappe/erpnext/pull/21587)
### Support
- [Help Articles on support portal](https://github.com/frappe/erpnext/pull/22194)
- [Issue Metrics and SLA Enhancements](https://github.com/frappe/erpnext/pull/21617)
- [Issue Summary Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
- [Issue Analytics Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
### Non-Profits
- [80G Certificates and Donations](https://docs.erpnext.com/docs/user/manual/en/non_profit/tax_exemption_80g_certificate)
#### Integrations
- [Woocommerce Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/woocommerce_integration)
- [Taxjar Integration](https://github.com/frappe/erpnext/pull/21047)
- [M-pesa Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/mpesa-integration)
- [Telephony feature using Twillio](https://github.com/frappe/erpnext/pull/24032)
- [Voice Call Settings](https://github.com/frappe/erpnext/pull/24126)
#### Other Enhancements and Fixes
- Accounting Dimensions in Budget Variance Report ([#19973](https://github.com/frappe/erpnext/pull/19973))
- "Sync Now" option in Plaid Settings ([#23602](https://github.com/frappe/erpnext/pull/23602))
- Custom Fields in POS ([#19876](https://github.com/frappe/erpnext/pull/19876))
- [Inter Warehouse Stock Transfer in Purchase Receipt](https://docs.erpnext.com/docs/user/manual/en/stock/articles/material-transfer-from-delivery-note)
- [Accounts Payable Report based on Payment Terms](https://docs.erpnext.com/docs/user/manual/en/accounts/accounting-reports)
- Configurable accounting dimension filters and validations ([#23912](https://github.com/frappe/erpnext/pull/23912))
- Territory tree in Customer Acquisition and Loyalty report ([#21668](https://github.com/frappe/erpnext/pull/21668))
- Allow Purchase Invoice Creation Without Purchase Order Checkbox in Supplier ([#20864](https://github.com/frappe/erpnext/pull/20864))
- Gross Profit In Quotation ([#21795](https://github.com/frappe/erpnext/pull/21795))
- Notify credit controller users for credit limit extension via Email ([#22213](https://github.com/frappe/erpnext/pull/22213))
- Run MRP at parent level in the production plan and make material transfer based upon materials availability ([#21545](https://github.com/frappe/erpnext/pull/21545))
- Balance Serial Nos in Stock Ledger report ([#23675](https://github.com/frappe/erpnext/pull/23675))
- Youtube interactions via Video ([#22867](https://github.com/frappe/erpnext/pull/22867))
- Consider Holiday List in Student Leave Application and Attendance ([#23388](https://github.com/frappe/erpnext/pull/23388))
- Patient appointment status changes ([#24201](https://github.com/frappe/erpnext/pull/24201))
- Sales order status filter added for production plan ([#23805](https://github.com/frappe/erpnext/pull/23805))
- Monthly attendance sheet report group by Department, Designation, Employee Grade and Branch ([#21331](https://github.com/frappe/erpnext/pull/21331))
- Upload Attendance template now have pre-filled holiday status ([#20947](https://github.com/frappe/erpnext/pull/20947))
- Provision to disable serial no and batch selector ([#24398](https://github.com/frappe/erpnext/pull/24398))
<details>
<summary>More</summary>
- Fetch Items from BOM in Stock Entry([#19498](https://github.com/frappe/erpnext/pull/19498))
- Supplier Sourced Items in BOM ([#23557](https://github.com/frappe/erpnext/pull/23557))
- Close Production Plan ([#23728](https://github.com/frappe/erpnext/pull/23728))
- Button to create Stock Entry for Drug Shortage ([#24012](https://github.com/frappe/erpnext/pull/24012))
- Added column cost center in Accounts Receivable report ([#23835](https://github.com/frappe/erpnext/pull/23835))
- Added jinja templating in Contract Template ([#24046](https://github.com/frappe/erpnext/pull/24046))
- Make account number length configurable ([#23845](https://github.com/frappe/erpnext/pull/23845))
- Add company and correct filter in bank reconciliation statement ([#23614](https://github.com/frappe/erpnext/pull/23614))
- Added Condition field in Pricing Rule ([#23014](https://github.com/frappe/erpnext/pull/23014))
- Open lead status on next contact date ([#23445](https://github.com/frappe/erpnext/pull/23445))
- [Tax Category in POS Profile](https://docs.erpnext.com/docs/user/manual/en/accounts/pos-profile)
- Added phone field in product Inquiry ([#23170](https://github.com/frappe/erpnext/pull/23170))
- Allow Discharge despite Unbilled Healthcare Services ([#24281](https://github.com/frappe/erpnext/pull/24281))
- Do Not Bill Patient Encounters for Inpatients ([#24355](https://github.com/frappe/erpnext/pull/24355))
- Autofill Supplier pop-up when only 1 Supplier in RFQ ([#22512](https://github.com/frappe/erpnext/pull/22512))
- Accounting entries for service item in Purchase receipt ([#22223](https://github.com/frappe/erpnext/pull/22223))
- Added Project in Sales Analytics report ([#23309](https://github.com/frappe/erpnext/pull/23309))
- Added all companies option in employee tree to view employee across all companies ([#22573](https://github.com/frappe/erpnext/pull/22573))
- Email Group Option In Email Campaign ([#22731](https://github.com/frappe/erpnext/pull/22731))
- Stock Report Enhancements ([#21727](https://github.com/frappe/erpnext/pull/21727))
- Added range for age in stock ageing ([#22622](https://github.com/frappe/erpnext/pull/22622))
- Report Summary in Financial Statement([#20876](https://github.com/frappe/erpnext/pull/20876))
- Added sequence id in routing for the completion of operations sequentially ([#23641](https://github.com/frappe/erpnext/pull/23641))
- Nested Set filtering for Accounting Dimension
- Add/Remove Items from submitted Sales/Purchase Order
- Provision to edit Item Details from Marketplace
- Scan Barcode in Purchase Receipt
- Disable Rounded Totals Checkbox for Salary Slips in HR Settings
- Renamed Loan Management to Loan on Desk Page ([#21877](https://github.com/frappe/erpnext/pull/21877))
- Added Expense Approver field in Employee master ([#22244](https://github.com/frappe/erpnext/pull/22244))
- Bill all hours by default on Timesheet ([#22155](https://github.com/frappe/erpnext/pull/22155))
- Unable to cancel employee advance ([#22374](https://github.com/frappe/erpnext/pull/22374))
- Status error in purchase invoice ([#22351](https://github.com/frappe/erpnext/pull/22351))
- Item-wise sales and purchase register export ([#22184](https://github.com/frappe/erpnext/pull/22184))
- Billing address in for Purchase documents ([#22233](https://github.com/frappe/erpnext/pull/22233))
- Handle canceled entries in financial statements ([#22231](https://github.com/frappe/erpnext/pull/22231))
- Default period start date and period end date for financial statements ([#22011](https://github.com/frappe/erpnext/pull/22011))
- Update Packed Items via Update Items in Sales Order ([#22392](https://github.com/frappe/erpnext/pull/22392))
- Hide delete company transactions button if not system manager ([#21839](https://github.com/frappe/erpnext/pull/21839))
- Skipping total row for tree-view reports ([#22350](https://github.com/frappe/erpnext/pull/22350))
- Cancelled entries in tds payable monthly report ([#22131](https://github.com/frappe/erpnext/pull/22131))
- Inter-company Invoice currency for multicurrency transactions ([#21984](https://github.com/frappe/erpnext/pull/21984))
- Filter batches based on item and warehouse in Pick List (develop) ([#21780](https://github.com/frappe/erpnext/pull/21780))
- Set cost center in Expense Claim child based on parent (if missing) ([#22175](https://github.com/frappe/erpnext/pull/22175))
- Item wise backdated stock entry posting for immutable ledger ([#22366](https://github.com/frappe/erpnext/pull/22366))
- Shopping cart UI fixes ([#22137](https://github.com/frappe/erpnext/pull/22137))
- Filter Leave Type based on allocation for a particular employee ([#22050](https://github.com/frappe/erpnext/pull/22050))
- Party validation for inter-warehouse transaction ([#22186](https://github.com/frappe/erpnext/pull/22186))
- Manufacturing dashboard and work order summary chart ([#21946](https://github.com/frappe/erpnext/pull/21946))
- IP Admission and Discharge, Minor fixes ([#21817](https://github.com/frappe/erpnext/pull/21817))
- Validation of Purchase Order against Material Request missing ([#22192](https://github.com/frappe/erpnext/pull/22192))
- Staffing Plan validation ([#22379](https://github.com/frappe/erpnext/pull/22379))
- Do not allow backdated stock transactions in previous fiscal year ([#21967](https://github.com/frappe/erpnext/pull/21967))
- Employee Advance Return not working ([#21812](https://github.com/frappe/erpnext/pull/21812))
- Added card for reports on education desk ([#21853](https://github.com/frappe/erpnext/pull/21853))
- Refactored project summary report ([#21943](https://github.com/frappe/erpnext/pull/21943))
- Revenue and Customer Count only in date range in Customer Acquitition Report ([#22210](https://github.com/frappe/erpnext/pull/22210))
- Alternative item not working for subcontract ([#22386](https://github.com/frappe/erpnext/pull/22386))
- Unable to create batched Item ([#22393](https://github.com/frappe/erpnext/pull/22393))
- Filters for the manufacturing reports ([#21960](https://github.com/frappe/erpnext/pull/21960))
- Raw material warehouse in Production Planning Report ([#21982](https://github.com/frappe/erpnext/pull/21982))
- Allowed LWP leave types to select in Leave Application even if there is no allocation against them ([#22197](https://github.com/frappe/erpnext/pull/22197))
- Report not working on parameter Grade ([#21951](https://github.com/frappe/erpnext/pull/21951))
- Allow to enter Relieving date if employee status is Left ([#22242](https://github.com/frappe/erpnext/pull/22242))
- Resetting lost reason in opportunity and quotation ([#22378](https://github.com/frappe/erpnext/pull/22378))
- Filtering issues in opening invoice creation tool ([#21969](https://github.com/frappe/erpnext/pull/21969))
- Set default reference Id for "On Previous Row Amount" and "On Previous Row Total" ([#22346](https://github.com/frappe/erpnext/pull/22346))
- UX date range field separated in from and to date fields. ([#21765](https://github.com/frappe/erpnext/pull/21765))
- Enable show_configure_button when shopping cart is enabled ([#22468](https://github.com/frappe/erpnext/pull/22468))
- Setup status indicators for Job Offer and Job Applicant (develop) ([#22445](https://github.com/frappe/erpnext/pull/22445))
- Item-wise sales history report ([#22783](https://github.com/frappe/erpnext/pull/22783))
- Setting filter for project in kanban board ([#22717](https://github.com/frappe/erpnext/pull/22717))
- Dashboard For Timesheet ([#22750](https://github.com/frappe/erpnext/pull/22750))
- Handle custom statuses for the pause SLA configuration ([#22349](https://github.com/frappe/erpnext/pull/22349))
- Quality Feedback and Template ([#22571](https://github.com/frappe/erpnext/pull/22571))
- Unable to change link from new lead to existing customer ([#22787](https://github.com/frappe/erpnext/pull/22787))
- Move Issue List actions under 'Actions' dropdown (ux) ([#22710](https://github.com/frappe/erpnext/pull/22710))
- Cost center should only show option of selected company ([#22598](https://github.com/frappe/erpnext/pull/22598))
- Serial No Rename does not affect Stock Ledger Entry ([#22746](https://github.com/frappe/erpnext/pull/22746))
- Descriptions not copied while creating Fees from Fee Structure ([#22792](https://github.com/frappe/erpnext/pull/22792))
- Company filter for cost_center and expense_account in all sales and purchase transactions ([#22478](https://github.com/frappe/erpnext/pull/22478))
- Arrangements of filters for reports accounts payable & receivable ([#22636](https://github.com/frappe/erpnext/pull/22636))
- Update the project after task deletion so that the % completed shows correct value ([#22591](https://github.com/frappe/erpnext/pull/22591))
- Block Invalid Serial No updates in Maintenance Schedule ([#22665](https://github.com/frappe/erpnext/pull/22665))
- Fetch item price in sales invoice based on it's validity ([#22563](https://github.com/frappe/erpnext/pull/22563))
- Add view ledger button for cancelled docs ([#22432](https://github.com/frappe/erpnext/pull/22432))
- Allow creating SLA documents even if SLA tracking is not enabled ([#22608](https://github.com/frappe/erpnext/pull/22608))
- Quotation list view blank if quotation_to field not set as a standard filter ([#22672](https://github.com/frappe/erpnext/pull/22672))
- Salary deductions report fixes ([#22397](https://github.com/frappe/erpnext/pull/22397))
22727))
- Incorrect delivered qty in Supplier-Wise Sales Analytics ([#22631](https://github.com/frappe/erpnext/pull/22631))
- Moved parent warehouse to top section also added a section break ([#22708](https://github.com/frappe/erpnext/pull/22708))
- Skip Progress and Completed by fields on Task Duplication ([#22565](https://github.com/frappe/erpnext/pull/22565))
- Incorrect stock after merging the items ([#22526](https://github.com/frappe/erpnext/pull/22526))
- Letter head not found in opening invoice creation tool ([#22488](https://github.com/frappe/erpnext/pull/22488))
- Cannot cancel asset and asset movement ([#22441](https://github.com/frappe/erpnext/pull/22441))
- Fetch project-related info in Timesheet ([#22423](https://github.com/frappe/erpnext/pull/22423))
- Currency symbol not showing as per company currency in stock balance report ([#22724](https://github.com/frappe/erpnext/pull/22724))
- Add default cost center in payment reconciliation JV ([#22614](https://github.com/frappe/erpnext/pull/22614))
- Stock Reconciliation Invalid Quantity for Batched Item ([#22726](https://github.com/frappe/erpnext/pull/22726))
- Project link not set in accounts other than profit and loss accounts ([#22051](https://github.com/frappe/erpnext/pull/22051))
- Buying price for non stock item in gross profit report ([#22616](https://github.com/frappe/erpnext/pull/22616))
- Multi currency payment reconciliation ([#22738](https://github.com/frappe/erpnext/pull/22738))
- Cannot cancel assets with repair pending ([#22440](https://github.com/frappe/erpnext/pull/22440))
- Reset homepage to home after unchecking products page ([#22736](https://github.com/frappe/erpnext/pull/22736))
- Generic Message in previous doc validation for buying and selling ([#22546](https://github.com/frappe/erpnext/pull/22546))
- Expense claim outstanding while making payment entry ([#22735](https://github.com/frappe/erpnext/pull/22735))
- Take parent cost center for child if no cost center at child in expense claim ([#22496](https://github.com/frappe/erpnext/pull/22496))
- Consider company fiscal year for getting balance ([#22577](https://github.com/frappe/erpnext/pull/22577))
- Pick List empty table and Serial-Batch items handling ([#22426](https://github.com/frappe/erpnext/pull/22426))
- Show total row in print format of financial statement ([#22693](https://github.com/frappe/erpnext/pull/22693))
- Set Root as Parent if no parent in new tree view node ([#22497](https://github.com/frappe/erpnext/pull/22497))
- Multiple pos issues ([#23725](https://github.com/frappe/erpnext/pull/23725))
- Calculate taxes if tax is based on item quantity and inclusive on item price ([#23001](https://github.com/frappe/erpnext/pull/23001))
- Contact us button not visible in the website for the non variant items ([#23217](https://github.com/frappe/erpnext/pull/23217))
- Not able to make Material Request from Sales Order ([#23669](https://github.com/frappe/erpnext/pull/23669))
- Capture advance payments in payment order ([#23256](https://github.com/frappe/erpnext/pull/23256))
- Program and Course Enrollment fixes ([#23333](https://github.com/frappe/erpnext/pull/23333))
- Cannot create asset if cwip disabled and account not set ([#23580](https://github.com/frappe/erpnext/pull/23580))
- Cannot merge pos invoices with inclusive tax ([#23541](https://github.com/frappe/erpnext/pull/23541))
- Do not allow Company as accounting dimension ([#23755](https://github.com/frappe/erpnext/pull/23755))
- Set value of wrong Bank Account field in Payment Entry ([#22302](https://github.com/frappe/erpnext/pull/22302))
- Reverse journal entry for multi-currency ([#23165](https://github.com/frappe/erpnext/pull/23165))
- Updated integrations desk page ([#23772](https://github.com/frappe/erpnext/pull/23772))
- Assessment Result child table not visible when accessed via Assessment Plan dashboard ([#22880](https://github.com/frappe/erpnext/pull/22880))
- Conversion factor fixes in Stock Entry ([#23407](https://github.com/frappe/erpnext/pull/23407))
- Total calculations for multi-currency RCM invoices ([#23072](https://github.com/frappe/erpnext/pull/23072))
- Show accounts in financial statements upto level 20 ([#23718](https://github.com/frappe/erpnext/pull/23718))
- Consolidated financial statement sums values into wrong parent ([#23288](https://github.com/frappe/erpnext/pull/23288))
- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
- Added missing reports on selling desk ([#23548](https://github.com/frappe/erpnext/pull/23548))
- Fixed heading in the mobile view ([#23145](https://github.com/frappe/erpnext/pull/23145))
- Misleading filters on Item tax Template Link field ([#22918](https://github.com/frappe/erpnext/pull/22918))
- Do not consider opening entries for TDS calculation ([#23597](https://github.com/frappe/erpnext/pull/23597))
- Attendance calendar map fix ([#23245](https://github.com/frappe/erpnext/pull/23245))
- Post cancellation accounting entry on posting date instead of current ([#23361](https://github.com/frappe/erpnext/pull/23361))
- Set Customer only if Contact is present ([#23704](https://github.com/frappe/erpnext/pull/23704))
- Add Delivery Note Count in Sales Invoice Dashboard ([#23161](https://github.com/frappe/erpnext/pull/23161))
- Breadcrumbs for Maintenance Visit and Schedule ([#23369](https://github.com/frappe/erpnext/pull/23369))
- Raise Error on over receipt/consumption for sub-contracted PR ([#23195](https://github.com/frappe/erpnext/pull/23195))
- Validate if company not set in the Payment Entry ([#23419](https://github.com/frappe/erpnext/pull/23419))
- Ignore company and bank account doctype while deleting company transactions ([#22953](https://github.com/frappe/erpnext/pull/22953))
- Sales funnel data is inconsistent ([#23110](https://github.com/frappe/erpnext/pull/23110))
- Credit Limit Email not working ([#23059](https://github.com/frappe/erpnext/pull/23059))
- Add Company in list fields to fetch for Expense Claim ([#23007](https://github.com/frappe/erpnext/pull/23007))
- Issue form cleaned up and renamed Minutes to First Response field ([#23066](https://github.com/frappe/erpnext/pull/23066))
- Quotation lost reason options fix ([#22814](https://github.com/frappe/erpnext/pull/22814))
- Tax amounts in HSN Wise Outward summary ([#23076](https://github.com/frappe/erpnext/pull/23076))
- Patient Appointment not able to save ([#23434](https://github.com/frappe/erpnext/pull/23434))
- Removed Working Hours field from Company ([#23009](https://github.com/frappe/erpnext/pull/23009))
- Added check-in time validation in the Inpatient Record - Transfer ([#22958](https://github.com/frappe/erpnext/pull/22958))
- Handle Blank from/to range in Numeric Item Attribute ([#23483](https://github.com/frappe/erpnext/pull/23483))
- Sequence Matcher error in Bank Reconciliation ([#23539](https://github.com/frappe/erpnext/pull/23539))
- Fixed Conversion Factor rate for the BOM Exploded Item ([#23151](https://github.com/frappe/erpnext/pull/23151))
- Payment Schedule not fetching ([#23476](https://github.com/frappe/erpnext/pull/23476))
- Validate if removed Item Attributes exist in variant items ([#22911](https://github.com/frappe/erpnext/pull/22911))
- Set default billing address for purchase documents ([#22950](https://github.com/frappe/erpnext/pull/22950))
- Added help link in navbar settings ([#22943](https://github.com/frappe/erpnext/pull/22943))
- Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt ([#23282](https://github.com/frappe/erpnext/pull/23282))
- Education Module fixes ([#23714](https://github.com/frappe/erpnext/pull/23714))
- Filter out cancelled entries in customer ledger summary ([#23205](https://github.com/frappe/erpnext/pull/23205))
- Fiscal Year and Tax Rates for Italy ([#23623](https://github.com/frappe/erpnext/pull/23623))
- Production Plan incorrect Work Order qty ([#23264](https://github.com/frappe/erpnext/pull/23264))
- Added new filters in the Batch-wise Balance History report ([#23676](https://github.com/frappe/erpnext/pull/23676))
- Update state code and union territory for Daman and Diu ([#22988](https://github.com/frappe/erpnext/pull/22988))
- Set Stock UOM in item while creating Material Request from Stock Entry ([#23436](https://github.com/frappe/erpnext/pull/23436))
- Sales Order to Purchase Order flow improvement ([#23357](https://github.com/frappe/erpnext/pull/23357))
- Student Admission and Student Applicant fixes ([#23515](https://github.com/frappe/erpnext/pull/23515))
- Loan disbursement amount validation ([#24000](https://github.com/frappe/erpnext/pull/24000))
- Making company address read-only in delivery note ([#23890](https://github.com/frappe/erpnext/pull/23890))
- BOM stock report color showing always red ([#23994](https://github.com/frappe/erpnext/pull/23994))
- Added filter for customer field in Issue ([#24051](https://github.com/frappe/erpnext/pull/24051))
- Added project link in timesheet form ([#23764](https://github.com/frappe/erpnext/pull/23764))
- Update integrations desk page ([#23767](https://github.com/frappe/erpnext/pull/23767))
- Place of supply change on address change ([#23941](https://github.com/frappe/erpnext/pull/23941))
- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23672](https://github.com/frappe/erpnext/pull/23672))
- Auto fetch serial nos with modified conversion factor ([#23854](https://github.com/frappe/erpnext/pull/23854))
- Default cost center in item master not set in stock entry ([#23877](https://github.com/frappe/erpnext/pull/23877))
- Incorrect de-link serial no and batch ([#23947](https://github.com/frappe/erpnext/pull/23947))
- Accounting for internal transfer invoices within same company ([#24021](https://github.com/frappe/erpnext/pull/24021))
- Multiple pricing rule with margin type as Percentage is not working ([#24205](https://github.com/frappe/erpnext/pull/24205))
- Added Purchase Order to Global Search ([#24055](https://github.com/frappe/erpnext/pull/24055))
- Cannot expand row in update items dialog ([#23839](https://github.com/frappe/erpnext/pull/23839))
- Maintain stock can't be changed it there is product bundle ([#23989](https://github.com/frappe/erpnext/pull/23989))
- SO to PO Mapping Issue ([#23820](https://github.com/frappe/erpnext/pull/23820))
- Asset with value zero doesn't show up in fixed asset register ([#24091](https://github.com/frappe/erpnext/pull/24091))
- Cannot save customer email & phone ([#23797](https://github.com/frappe/erpnext/pull/23797))
- Incorrect balance value in stock balance report ([#24048](https://github.com/frappe/erpnext/pull/24048))
- Payment Terms not fetched in Purchase Invoice from Purchase Receipt ([#23735](https://github.com/frappe/erpnext/pull/23735))
- Fix for LMS Sign Up link ([#23743](https://github.com/frappe/erpnext/pull/23743))
- Incorrect stock quantity if 'Allow Multiple Material Consumption… ([#24116](https://github.com/frappe/erpnext/pull/24116))
- Added wrong absent days calculation in salary slip ([#23897](https://github.com/frappe/erpnext/pull/23897))
- Purchase receipt to purchase invoice bill date mapping ([#23967](https://github.com/frappe/erpnext/pull/23967))
- Overriding po ([#24022](https://github.com/frappe/erpnext/pull/24022))
- Do not cancel reference document on Quality Inspection cancellation ([#24198](https://github.com/frappe/erpnext/pull/24198))
- Get formatted value in 'taxes' print template ([#24035](https://github.com/frappe/erpnext/pull/24035))
- Don't overrule Item Price via Pricing Rule Rate if 0 ([#23636](https://github.com/frappe/erpnext/pull/23636))
- Job card error handling for operations field ([#23991](https://github.com/frappe/erpnext/pull/23991))
- Validation for journal entry with 0 debit and credit values ([#23975](https://github.com/frappe/erpnext/pull/23975))
- Check if customer exists in product listing ([#24030](https://github.com/frappe/erpnext/pull/24030))
- Asset finance book posting date fix ([#23778](https://github.com/frappe/erpnext/pull/23778))
- Same source and target tables in Status Updater's update query ([#24110](https://github.com/frappe/erpnext/pull/24110))
- Asset finance book depreciation posting date fix ([#23833](https://github.com/frappe/erpnext/pull/23833))
- Ignore exception during leave ledger creation from patch ([#24005](https://github.com/frappe/erpnext/pull/24005))
- Added link of bank reconciliation and clearance in accounting desk page ([#23850](https://github.com/frappe/erpnext/pull/23850))
- Sales invoice add button from sales order dashboard ([#24077](https://github.com/frappe/erpnext/pull/24077))
- Incorrect calculation for consumed qty for subcontract item ([#23257](https://github.com/frappe/erpnext/pull/23257))
- Incorrect required_qty in Production Planning Report ([#24074](https://github.com/frappe/erpnext/pull/24074))
- Email digest user not found ([#23949](https://github.com/frappe/erpnext/pull/23949))
- Delete Receive at Warehouse entry on cancellation of Send to War… ([#24115](https://github.com/frappe/erpnext/pull/24115))
- Added TDS Payable account number and an error message ([#24065](https://github.com/frappe/erpnext/pull/24065))
- Override field_map for job card gantt ([#24155](https://github.com/frappe/erpnext/pull/24155))
- Old shopify order syncing date ([#23990](https://github.com/frappe/erpnext/pull/23990))
- Shipping chanrges not sync in erpnext from shopify ([#24114](https://github.com/frappe/erpnext/pull/24114))
- GSTR B2C report ([#24039](https://github.com/frappe/erpnext/pull/24039))
- Ignore cancelled entries in stock balance report ([#23757](https://github.com/frappe/erpnext/pull/23757))
- Stock ageing report not working ([#23923](https://github.com/frappe/erpnext/pull/23923))
- Incorrect assign to in Maintenance Schedule ([#23831](https://github.com/frappe/erpnext/pull/23831))
- Improve UX of DATEV report ([#23892](https://github.com/frappe/erpnext/pull/23892))
- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
- dDouble exception in payroll ([#24078](https://github.com/frappe/erpnext/pull/24078))
- Make asset dashboard charts public ([#23751](https://github.com/frappe/erpnext/pull/23751))
- Don't copy terms and discount from SO to PO ([#23903](https://github.com/frappe/erpnext/pull/23903))
- Ignore doctypes on company transaction delete ([#23864](https://github.com/frappe/erpnext/pull/23864))
- Error handling in Upload Attendance ([#23907](https://github.com/frappe/erpnext/pull/23907))
- Tax template update on customer address change ([#24160](https://github.com/frappe/erpnext/pull/24160))
- Not able to save bom ([#23910](https://github.com/frappe/erpnext/pull/23910))
- Enable Allow Auto Repeat for standard doctypes having auto_repeat field ([#23776](https://github.com/frappe/erpnext/pull/23776))
- Place of Supply fix in Sales Invoices ([#23785](https://github.com/frappe/erpnext/pull/23785))
- Opening invoices in GSTR-1 report ([#24117](https://github.com/frappe/erpnext/pull/24117))
- Partial serial no return issue ([#24208](https://github.com/frappe/erpnext/pull/24208))
- Import taxjar globally in the taxjar_integration module ([#24027](https://github.com/frappe/erpnext/pull/24027))
- Payroll attendance error ([#23887](https://github.com/frappe/erpnext/pull/23887))
- Loan application link on creating loan ([#23937](https://github.com/frappe/erpnext/pull/23937))
- POS item search includes non stock items ([#23914](https://github.com/frappe/erpnext/pull/23914))
- Paid amount in Sales Invoice POS return resets to 0 ([#24057](https://github.com/frappe/erpnext/pull/24057))
- Fiscal year can be shorter than 12 months ([#23838](https://github.com/frappe/erpnext/pull/23838))
- Loan repayment type option remove ([#23582](https://github.com/frappe/erpnext/pull/23582))
- Item wise tax calculation ([#23744](https://github.com/frappe/erpnext/pull/23744))
- Enabling track changes for stock settings ([#23982](https://github.com/frappe/erpnext/pull/23982))
- Added link of bank reconciliation and clearance in accounting desk page ([#23809](https://github.com/frappe/erpnext/pull/23809))
- Location data on Asset to use command(make_demo) ([#23825](https://github.com/frappe/erpnext/pull/23825))
- Handle Account and Item None not found in Opening Invoice Creation Tool ([#23559](https://github.com/frappe/erpnext/pull/23559))
- Multiple subcontracting issues ([#23662](https://github.com/frappe/erpnext/pull/23662))
- Sequence id override with workstation column ([#23810](https://github.com/frappe/erpnext/pull/23810))
- Leave policy dashboard fix and roles ([#24170](https://github.com/frappe/erpnext/pull/24170))
- Scan barcode does not update barcode item field in sales order ([#24090](https://github.com/frappe/erpnext/pull/24090))
- Item price duplicate checking ([#23408](https://github.com/frappe/erpnext/pull/23408))
- Tax template update on supplier change for India ([#24060](https://github.com/frappe/erpnext/pull/24060))
- Consumed qty logic for subcontracted raw materials ([#23314](https://github.com/frappe/erpnext/pull/23314))
- Finance book not getting added in journal Entry of asset value adjustment ([#24100](https://github.com/frappe/erpnext/pull/24100))
- Set proper state code in ewaybill JSON when GST category is SEZ ([#23953](https://github.com/frappe/erpnext/pull/23953))
- Copying po no when mapping doc ([#23729](https://github.com/frappe/erpnext/pull/23729))
- Duplicate items validation for POS Invoice when allow multiple items is disabled ([#23896](https://github.com/frappe/erpnext/pull/23896))
- Do not allow Company as accounting dimension ([#23749](https://github.com/frappe/erpnext/pull/23749))
- Validation for duplicate Tax Category ([#23978](https://github.com/frappe/erpnext/pull/23978))
- Therapy plan and session fixes ([#23817](https://github.com/frappe/erpnext/pull/23817))
- Pricing rule with transaction not working for additional product ([#24053](https://github.com/frappe/erpnext/pull/24053))
- Inpatient Medication Order and Entry fixes ([#23799](https://github.com/frappe/erpnext/pull/23799))
- Avoid using SQL query to get fiscal year dates ([#24050](https://github.com/frappe/erpnext/pull/24050))
- Auto Statewise gst tax template ([#23832](https://github.com/frappe/erpnext/pull/23832))
- On save sequence id column override with workstation ([#23812](https://github.com/frappe/erpnext/pull/23812))
- Multiple pricing rules are not working on selling side ([#22711](https://github.com/frappe/erpnext/pull/22711))
- Salary slip popup error ([#24192](https://github.com/frappe/erpnext/pull/24192))
- Multiple pricing rule with margin type as Percentage is not working ([#24204](https://github.com/frappe/erpnext/pull/24204))
- Allow statistical component in salary structure. ([#24424](https://github.com/frappe/erpnext/pull/24424))
- Set current asset value before calculating difference amount ([#24119](https://github.com/frappe/erpnext/pull/24119))
- To use Stock UoM in BOM Stock Report ([#24339](https://github.com/frappe/erpnext/pull/24339))
- Accounting entries of asset when submitting purchase receipt ([#24191](https://github.com/frappe/erpnext/pull/24191))
- Batch/Serial Selector for Scanned Batched Item ([#24338](https://github.com/frappe/erpnext/pull/24338))
- Link timesheets with corresponding projects ([#24346](https://github.com/frappe/erpnext/pull/24346))
- Material request wrong status issue ([#24019](https://github.com/frappe/erpnext/pull/24019))
- UX issues in e-invoicing ([#24358](https://github.com/frappe/erpnext/pull/24358))
- Company Wise Valuation Rate for RM in BOM ([#24324](https://github.com/frappe/erpnext/pull/24324))
- Stock ageing should not take cancelled stock entries. ([#24437](https://github.com/frappe/erpnext/pull/24437))
- Partial loan security unpledging ([#24252](https://github.com/frappe/erpnext/pull/24252))
- Asset depreciation ledger ([#24226](https://github.com/frappe/erpnext/pull/24226))
- Back Update from QC based on Batch No ([#24329](https://github.com/frappe/erpnext/pull/24329))
- Fix for not having fiscal year while creating new company ([#24130](https://github.com/frappe/erpnext/pull/24130))
- E-invoice print format not showing other charges ([#24474](https://github.com/frappe/erpnext/pull/24474))
- Tax template update on customer address change ([#24146](https://github.com/frappe/erpnext/pull/24146))
- Do not manufacture same serial no multiple times ([#24164](https://github.com/frappe/erpnext/pull/24164))
- Ignore group cost center validation for period closing voucher ([#24375](https://github.com/frappe/erpnext/pull/24375))
- Partial serial no return issue ([#24207](https://github.com/frappe/erpnext/pull/24207))
- GSTR-1 double entry issue ([#24376](https://github.com/frappe/erpnext/pull/24376))
- Not able to create dunning from sales invoice ([#24349](https://github.com/frappe/erpnext/pull/24349))
- Set company in leave allocation and leave ledger entry ([#24296](https://github.com/frappe/erpnext/pull/24296))
- Allow leave policy assignment to be canceled. ([#24265](https://github.com/frappe/erpnext/pull/24265))
- Removed all day event from shift assignment calendar ([#24397](https://github.com/frappe/erpnext/pull/24397))
- Tax calculation on salary slip for the first month ([#24272](https://github.com/frappe/erpnext/pull/24272))
- Validate tax template for tax category ([#24402](https://github.com/frappe/erpnext/pull/24402))
- Numeric/Non-numeric QI UX ([#24517](https://github.com/frappe/erpnext/pull/24517))
- Finished good produced qty validation ([#24220](https://github.com/frappe/erpnext/pull/24220))
- Incorrect serial no in the subcontracted purchase receipt ([#24354](https://github.com/frappe/erpnext/pull/24354))
- Don't validate warehouse values between Material Request and Stock Entry ([#24294](https://github.com/frappe/erpnext/pull/24294))
- Don't cancel job card if manufacturing entry has made ([#24063](https://github.com/frappe/erpnext/pull/24063))
- Subscription prepaid date validation ([#24356](https://github.com/frappe/erpnext/pull/24356))
- Payment Period based on invoice date report fix/refactor ([#24378](https://github.com/frappe/erpnext/pull/24378))
- Drop ship partial order fixed ([#24072](https://github.com/frappe/erpnext/pull/24072))
- Payment entry multi-currency issue ([#24332](https://github.com/frappe/erpnext/pull/24332))
- Multiple pricing rule issue ([#24515](https://github.com/frappe/erpnext/pull/24515))
- Last purchase rate not updating when voucher cancelled if only one voucher is present ([#24322](https://github.com/frappe/erpnext/pull/24322))
- Do not cancel reference document on Quality Inspection cancellation ([#24197](https://github.com/frappe/erpnext/pull/24197))
- Refactored fetching & validating address from erpnext rather than gst portal ([#24297](https://github.com/frappe/erpnext/pull/24297))
- Opportunity Status fix ([#22944](https://github.com/frappe/erpnext/pull/22944))
- Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644))
- Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649))
- Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580))
- Repost incompleted backdated transactions ([#24991](https://github.com/frappe/erpnext/pull/24991))
- Unequal debit and credit issue on RCM Invoice ([#24838](https://github.com/frappe/erpnext/pull/24838))
- Period list for exponential smoothing forecasting report ([#24983](https://github.com/frappe/erpnext/pull/24983))
- POS Opening Entry with empty balance detail rows ([#24891](https://github.com/frappe/erpnext/pull/24891))
- Use account_name only in consolidated report ([#24840](https://github.com/frappe/erpnext/pull/24840))
- Validation of job card in stock entry ([#24882](https://github.com/frappe/erpnext/pull/24882))
- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24918](https://github.com/frappe/erpnext/pull/24918))
- TDS check getting checked after reload ([#24973](https://github.com/frappe/erpnext/pull/24973))
- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
- Allow zero valuation in stock reconciliation ([#24985](https://github.com/frappe/erpnext/pull/24985))
- Simplified logic for additional salary ([#24907](https://github.com/frappe/erpnext/pull/24907))
- Allow to select item code in batch naming ([#24825](https://github.com/frappe/erpnext/pull/24825))
- Membership renewal validation (#24963) ([#24964](https://github.com/frappe/erpnext/pull/24964))
</details>

View File

@ -659,6 +659,7 @@ class AccountsController(TransactionBase):
'dr_or_cr': dr_or_cr, 'dr_or_cr': dr_or_cr,
'unadjusted_amount': flt(d.advance_amount), 'unadjusted_amount': flt(d.advance_amount),
'allocated_amount': flt(d.allocated_amount), 'allocated_amount': flt(d.allocated_amount),
'precision': d.precision('advance_amount'),
'exchange_rate': (self.conversion_rate 'exchange_rate': (self.conversion_rate
if self.party_account_currency != self.company_currency else 1), if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total 'grand_total': (self.base_grand_total

View File

@ -406,8 +406,7 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self): def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"): for d in self.get("items"):
if d.conversion_factor: d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \

View File

@ -113,10 +113,12 @@ class calculate_taxes_and_totals(object):
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
if item.discount_amount and not item.discount_percentage: if item.discount_amount and not item.discount_percentage:
item.rate -= item.discount_amount item.rate = item.rate_with_margin - item.discount_amount
else: else:
item.discount_amount = item.rate_with_margin - item.rate item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0: elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount: elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@ -147,7 +149,9 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) 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_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
@ -338,7 +342,9 @@ class calculate_taxes_and_totals(object):
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) 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 return current_tax_amount
@ -440,8 +446,9 @@ class calculate_taxes_and_totals(object):
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self): def _cleanup(self):
for tax in self.doc.get("taxes"): if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) 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): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:

View File

@ -25,7 +25,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
if not filters: filters = [] if not filters: filters = []
if doctype in ['Supplier Quotation', 'Purchase Invoice', 'Quotation']: if doctype in ['Supplier Quotation', 'Purchase Invoice']:
filters.append((doctype, 'docstatus', '<', 2)) filters.append((doctype, 'docstatus', '<', 2))
else: else:
filters.append((doctype, 'docstatus', '=', 1)) filters.append((doctype, 'docstatus', '=', 1))

View File

@ -22,7 +22,7 @@ class ShopifySettings(unittest.TestCase):
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
# use the fixture data # use the fixture data
import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer") frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order") frappe.reload_doctype("Sales Order")

View File

@ -39,11 +39,13 @@ frappe.ui.form.on('Patient Assessment', {
}, },
set_score_range: function(frm) { set_score_range: function(frm) {
let options = []; let options = [''];
for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) { for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) {
options.push(i); options.push(i);
} }
frappe.meta.get_docfield('Patient Assessment Sheet', 'score', frm.doc.name).options = [''].concat(options); frm.fields_dict.assessment_sheet.grid.update_docfield_property(
'score', 'options', options
);
}, },
calculate_total_score: function(frm, cdt, cdn) { calculate_total_score: function(frm, cdt, cdn) {

View File

@ -58,8 +58,12 @@ frappe.ui.form.on('Therapy Plan', {
} }
if (frm.doc.therapy_plan_template) { if (frm.doc.therapy_plan_template) {
frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1; frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1; 'therapy_type', 'read_only', 1
);
frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
'no_of_sessions', 'read_only', 1
);
} }
}, },

View File

@ -262,7 +262,8 @@ doc_events = {
], ],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": "erpnext.regional.check_deletion_permission",
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_document_name" "erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
] ]
}, },
"Purchase Invoice": { "Purchase Invoice": {

View File

@ -35,7 +35,8 @@ class Attendance(Document):
and docstatus != 2 and docstatus != 2
""", (self.employee, getdate(self.attendance_date), self.name)) """, (self.employee, getdate(self.attendance_date), self.name))
if res: if res:
frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def check_leave_record(self): def check_leave_record(self):
leave_record = frappe.db.sql(""" leave_record = frappe.db.sql("""

View File

@ -200,7 +200,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:42:47.321368", "modified": "2021-03-31 22:31:53.746659",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Advance", "name": "Employee Advance",

View File

@ -154,7 +154,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:45:27.948207", "modified": "2021-03-31 22:32:55.492327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Encashment", "name": "Leave Encashment",

View File

@ -23,6 +23,7 @@
"rate_of_interest", "rate_of_interest",
"is_secured_loan", "is_secured_loan",
"disbursement_date", "disbursement_date",
"closure_date",
"disbursed_amount", "disbursed_amount",
"column_break_11", "column_break_11",
"maximum_loan_amount", "maximum_loan_amount",
@ -348,12 +349,18 @@
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "closure_date",
"fieldtype": "Date",
"label": "Closure Date",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-24 12:27:23.208240", "modified": "2021-04-10 09:28:21.946972",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@ -523,33 +523,7 @@ class TestLoan(unittest.TestCase):
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self): def test_penalty(self):
pledge = [{ loan, amounts = create_loan_scenario_for_penalty(self)
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
# 30 days - grace period # 30 days - grace period
penalty_days = 30 - 4 penalty_days = 30 - 4
penalty_applicable_amount = flt(amounts['interest_amount']/2) penalty_applicable_amount = flt(amounts['interest_amount']/2)
@ -559,8 +533,28 @@ class TestLoan(unittest.TestCase):
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
self.assertEquals(loan.loan_amount, 1000000)
self.assertEquals(calculated_penalty_amount, penalty_amount) self.assertEquals(calculated_penalty_amount, penalty_amount)
def test_penalty_repayment(self):
loan, dummy = create_loan_scenario_for_penalty(self)
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00')
first_penalty = 10000
second_penalty = amounts['penalty_amount'] - 10000
repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000)
repayment_entry.submit()
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01')
self.assertEquals(amounts['penalty_amount'], second_penalty)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty)
repayment_entry.submit()
amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02')
self.assertEquals(amounts['penalty_amount'], 0)
def test_loan_write_off_limit(self): def test_loan_write_off_limit(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
@ -651,6 +645,32 @@ class TestLoan(unittest.TestCase):
amounts = calculate_amounts(loan.name, add_days(last_date, 5)) amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
def create_loan_scenario_for_penalty(doc):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
paid_amount = amounts['interest_amount']/2
repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5),
paid_amount)
repayment_entry.submit()
return loan, amounts
def create_loan_accounts(): def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):

View File

@ -20,6 +20,10 @@
"cost_center", "cost_center",
"customer_details_section", "customer_details_section",
"bank_account", "bank_account",
"disbursement_references_section",
"reference_date",
"column_break_17",
"reference_number",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -126,12 +130,31 @@
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "disbursement_references_section",
"fieldtype": "Section Break",
"label": "Disbursement References"
},
{
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Reference Date"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-06 10:04:30.882322", "modified": "2021-04-10 10:03:41.502210",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Disbursement", "name": "Loan Disbursement",

View File

@ -239,14 +239,16 @@
{ {
"fieldname": "total_penalty_paid", "fieldname": "total_penalty_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Total Penalty Paid", "label": "Total Penalty Paid",
"options": "Company:company:default_currency" "options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-05 13:45:19.137896", "modified": "2021-04-10 10:00:31.859076",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",

View File

@ -75,7 +75,7 @@ class LoanRepayment(AccountsController):
"docstatus": 1, "against_loan": self.against_loan}, 'posting_date') "docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
if future_repayment_date: if future_repayment_date:
frappe.throw("Repayment already made till date {0}".format(getdate(future_repayment_date))) frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date)))
def validate_amount(self): def validate_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -83,10 +83,6 @@ class LoanRepayment(AccountsController):
if not self.amount_paid: if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero")) frappe.throw(_("Amount paid cannot be zero"))
if not self.shortfall_amount and self.amount_paid < self.penalty_amount:
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg)
def book_unaccrued_interest(self): def book_unaccrued_interest(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2 precision = cint(frappe.db.get_default("currency_precision")) or 2
if self.total_interest_paid > self.interest_payable: if self.total_interest_paid > self.interest_payable:
@ -231,6 +227,14 @@ class LoanRepayment(AccountsController):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan) loan_details = frappe.get_doc("Loan", self.against_loan)
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
self.against_loan)
elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
else:
remarks = _("Repayment against Loan: ") + self.against_loan
if self.total_penalty_paid: if self.total_penalty_paid:
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
@ -271,7 +275,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.amount_paid, "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
@ -287,7 +291,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid, "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Repayment against Loan: ") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
@ -338,6 +342,18 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
return unpaid_accrued_entries return unpaid_accrued_entries
def get_penalty_details(against_loan):
penalty_details = frappe.db.sql("""
SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount
FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment`
where against_loan = %s) and docstatus = 1 and against_loan = %s
""", (against_loan, against_loan))
if penalty_details:
return penalty_details[0][0], flt(penalty_details[0][1])
else:
return None, 0
# This function returns the amounts that are payable at the time of loan repayment based on posting date # This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
@ -348,6 +364,7 @@ def get_amounts(amounts, against_loan, posting_date):
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date) accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date)
computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan)
pending_accrual_entries = {} pending_accrual_entries = {}
total_pending_interest = 0 total_pending_interest = 0
@ -362,8 +379,13 @@ def get_amounts(amounts, against_loan, posting_date):
# and if no_of_late days are positive then penalty is levied # and if no_of_late days are positive then penalty is levied
due_date = add_days(entry.posting_date, 1) due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date, due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days)
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
# Consider one day after already calculated penalty
if computed_penalty_date and getdate(computed_penalty_date) >= due_date_after_grace_period:
due_date_after_grace_period = add_days(computed_penalty_date, 1)
no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular':
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
@ -401,7 +423,7 @@ def get_amounts(amounts, against_loan, posting_date):
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
amounts["interest_amount"] = flt(total_pending_interest, precision) amounts["interest_amount"] = flt(total_pending_interest, precision)
amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_datetime, flt from frappe.utils import get_datetime, flt, getdate
import json import json
from six import iteritems from six import iteritems
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
@ -113,7 +113,11 @@ class LoanSecurityUnpledge(Document):
pledged_qty += qty pledged_qty += qty
if not pledged_qty: if not pledged_qty:
frappe.db.set_value('Loan', self.loan, 'status', 'Closed') frappe.db.set_value('Loan', self.loan,
{
'status': 'Closed',
'closure_date': getdate()
})
@frappe.whitelist() @frappe.whitelist()
def get_pledged_security_qty(loan): def get_pledged_security_qty(loan):

View File

@ -93,15 +93,15 @@ class TestBOM(unittest.TestCase):
base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) 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")) base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
# test amounts in selected currency # test amounts in selected currency, almostEqual checks for 7 digits by default
self.assertEqual(bom.operating_cost, op_cost) self.assertAlmostEqual(bom.operating_cost, op_cost)
self.assertEqual(bom.raw_material_cost, raw_material_cost) self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
self.assertEqual(bom.total_cost, raw_material_cost + op_cost) self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
# test amounts in selected currency # test amounts in selected currency
self.assertEqual(bom.base_operating_cost, base_op_cost) self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(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_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): 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) frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)

View File

@ -433,6 +433,7 @@ def make_material_request(source_name, target_doc=None):
def make_stock_entry(source_name, target_doc=None): def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse target.t_warehouse = source_parent.wip_warehouse
target.conversion_factor = 1
def set_missing_values(source, target): def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture" target.purpose = "Material Transfer for Manufacture"

View File

@ -11,10 +11,9 @@ frappe.ui.form.on('Routing', {
}, },
display_sequence_id_column: function(frm) { display_sequence_id_column: function(frm) {
frappe.meta.get_docfield("BOM Operation", "sequence_id", frm.fields_dict.operations.grid.update_docfield_property(
frm.doc.name).in_list_view = true; 'sequence_id', 'in_list_view', 1
);
frm.fields_dict.operations.grid.refresh();
}, },
calculate_operating_cost: function(frm, child) { calculate_operating_cost: function(frm, child) {

View File

@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2020-07-08 14:05:56.197563", "modified": "2020-06-29 20:25:36.899106",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing", "name": "Manufacturing",

View File

@ -756,11 +756,18 @@ erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes 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.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings 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_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.setup_uae_vat_fields erpnext.patches.v13_0.setup_uae_vat_fields
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') 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.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 erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing

View File

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

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'Italy'})
if not company:
return
custom_fields = {
'Sales Invoice': [
dict(fieldname='type_of_document', label='Type of Document',
fieldtype='Select', insert_after='customer_fiscal_code',
options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'),
]
}
create_custom_fields(custom_fields, update=True)

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

View File

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

View File

@ -0,0 +1,19 @@
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 = {
'Delivery Note': [
dict(fieldname='gst_category', label='GST Category',
fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1,
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
fetch_from='customer.gst_category', fetch_if_empty=1),
]
}
create_custom_fields(custom_fields, update=True)

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

View File

@ -1,6 +1,7 @@
import frappe import frappe
def execute(): def execute():
frappe.reload_doc('custom', 'doctype', 'custom_field')
company = frappe.get_all('Company', filters = {'country': 'India'}) company = frappe.get_all('Company', filters = {'country': 'India'})
if not company: if not company:
return return

View File

@ -8,36 +8,39 @@ from erpnext.regional.india.setup import setup
def execute(): def execute():
doctypes = ['salary_component', doctypes = ['salary_component',
'Employee Tax Exemption Declaration', 'Employee Tax Exemption Declaration',
'Employee Tax Exemption Proof Submission', 'Employee Tax Exemption Proof Submission',
'Employee Tax Exemption Declaration Category', 'Employee Tax Exemption Declaration Category',
'Employee Tax Exemption Proof Submission Detail' 'Employee Tax Exemption Proof Submission Detail',
] 'gratuity_rule',
'gratuity_rule_slab',
'gratuity_applicable_component'
]
for doctype in doctypes: for doctype in doctypes:
frappe.reload_doc('Payroll', 'doctype', doctype) frappe.reload_doc('Payroll', 'doctype', doctype)
reports = ['Professional Tax Deductions', 'Provident Fund Deductions'] reports = ['Professional Tax Deductions', 'Provident Fund Deductions']
for report in reports: for report in reports:
frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report)
frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India": if erpnext.get_region() == "India":
setup(patch=True) setup(patch=True)
if frappe.db.exists("Salary Component", "Income Tax"): if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
if frappe.db.exists("Salary Component", "TDS"): if frappe.db.exists("Salary Component", "TDS"):
frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1) frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1)
components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1) components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1)
for component in components: for component in components:
frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1) frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1)
if erpnext.get_region() == "India": if erpnext.get_region() == "India":
if frappe.db.exists("Salary Component", "Provident Fund"): if frappe.db.exists("Salary Component", "Provident Fund"):
frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund") frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund")
if frappe.db.exists("Salary Component", "Professional Tax"): if frappe.db.exists("Salary Component", "Professional Tax"):
frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax") frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax")

View File

@ -11,4 +11,8 @@ def execute():
if not company: if not company:
return return
frappe.reload_doc('accounts', 'doctype', 'pos_invoice')
frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item')
make_custom_fields() make_custom_fields()

View File

@ -0,0 +1,8 @@
import frappe
def execute():
frappe.db.sql("""
update tabCustomer
set represents_company = NULL
where represents_company = ''
""")

View File

@ -18,6 +18,7 @@ def execute():
for old_dt, new_dt in doctypes.items(): for old_dt, new_dt in doctypes.items():
if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt): 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.rename_doc('DocType', old_dt, new_dt, force=True)
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt)) frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt))
frappe.delete_doc_if_exists('DocType', old_dt) frappe.delete_doc_if_exists('DocType', old_dt)
@ -36,6 +37,18 @@ def execute():
SET parentfield = %(parentfield)s SET parentfield = %(parentfield)s
""".format(doctype), {'parentfield': parentfield}) """.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 # rename field
frappe.reload_doc('healthcare', 'doctype', 'lab_test') frappe.reload_doc('healthcare', 'doctype', 'lab_test')
if frappe.db.has_column('Lab Test', 'special_toggle'): if frappe.db.has_column('Lab Test', 'special_toggle'):

View File

@ -20,9 +20,11 @@ def execute():
frappe.clear_cache() frappe.clear_cache()
frappe.flags.warehouse_account_map = {} frappe.flags.warehouse_account_map = {}
company_list = []
data = frappe.db.sql(''' data = frappe.db.sql('''
SELECT SELECT
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
FROM FROM
`tabStock Ledger Entry` `tabStock Ledger Entry`
WHERE WHERE
@ -36,6 +38,9 @@ def execute():
total_sle = len(data) total_sle = len(data)
i = 0 i = 0
for d in data: for d in data:
if d.company not in company_list:
company_list.append(d.company)
update_entries_after({ update_entries_after({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "warehouse": d.warehouse,
@ -53,8 +58,10 @@ def execute():
print("Reposting General Ledger Entries...") print("Reposting General Ledger Entries...")
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): if data:
update_gl_entries_after(posting_date, posting_time, company=row.name) for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
if row.name in company_list:
update_gl_entries_after(posting_date, posting_time, company=row.name)
frappe.db.auto_commit_on_many_writes = 0 frappe.db.auto_commit_on_many_writes = 0

View File

@ -6,8 +6,9 @@ def execute():
if "Healthcare" not in frappe.get_active_domains(): if "Healthcare" not in frappe.get_active_domains():
return return
frappe.reload_doc("healthcare", "doctype", "Therapy Session")
frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order") frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
frappe.reload_doc("healthcare", "doctype", "Therapy Session")
frappe.reload_doc("healthcare", "doctype", "Clinical Procedure")
frappe.reload_doc("healthcare", "doctype", "Patient History Settings") 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 Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type") frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")

View File

@ -2,11 +2,15 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from erpnext.regional.united_arab_emirates.setup import setup from erpnext.regional.united_arab_emirates.setup import setup
def execute(): def execute():
company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'}) company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'})
if not company: if not company:
return return
frappe.reload_doc('regional', 'report', 'uae_vat_201')
frappe.reload_doc('regional', 'doctype', 'uae_vat_settings')
frappe.reload_doc('regional', 'doctype', 'uae_vat_account')
setup() setup()

View File

@ -175,7 +175,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:45:48.566756", "modified": "2021-03-31 22:33:59.098532",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Additional Salary", "name": "Additional Salary",

View File

@ -147,7 +147,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:46:22.465521", "modified": "2021-03-31 22:35:08.940087",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Employee Benefit Application", "name": "Employee Benefit Application",

View File

@ -144,7 +144,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 15:51:51.489269", "modified": "2021-03-31 22:37:21.024625",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Employee Benefit Claim", "name": "Employee Benefit Claim",

View File

@ -94,7 +94,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:48:00.919839", "modified": "2021-03-31 22:38:20.332316",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Employee Incentive", "name": "Employee Incentive",

View File

@ -119,7 +119,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 20:41:57.387749", "modified": "2021-03-31 22:39:59.237361",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Employee Tax Exemption Declaration", "name": "Employee Tax Exemption Declaration",

View File

@ -142,7 +142,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 20:48:32.639885", "modified": "2021-03-31 22:41:13.723339",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Employee Tax Exemption Proof Submission", "name": "Employee Tax Exemption Proof Submission",

View File

@ -104,7 +104,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 20:53:33.323712", "modified": "2021-03-31 22:42:08.139520",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Income Tax Slab", "name": "Income Tax Slab",

View File

@ -567,6 +567,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True):
if publish_progress: if publish_progress:
frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)),
title = _("Creating Salary Slips...")) title = _("Creating Salary Slips..."))
else: else:
salary_slips_not_created.append(emp) salary_slips_not_created.append(emp)

View File

@ -105,7 +105,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 14:50:29.401020", "modified": "2021-03-31 22:43:28.363644",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Retention Bonus", "name": "Retention Bonus",

View File

@ -119,6 +119,7 @@ frappe.ui.form.on("Salary Slip", {
frm.set_df_property('exchange_rate', 'hidden', 1); frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" ); frm.set_df_property("exchange_rate", "description", "" );
} }
}
} }
}, },

View File

@ -631,7 +631,7 @@
"idx": 9, "idx": 9,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 15:39:28.817166", "modified": "2021-03-31 22:44:09.772331",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Slip", "name": "Salary Slip",

View File

@ -598,10 +598,10 @@ class SalarySlip(TransactionBase):
continue continue
if ( if (
not d.additional_salary (not d.additional_salary
and (not additional_salary or additional_salary.overwrite) and (not additional_salary or additional_salary.overwrite))
or additional_salary or (additional_salary
and additional_salary.name == d.additional_salary and additional_salary.name == d.additional_salary)
): ):
component_row = d component_row = d
break break
@ -611,7 +611,7 @@ class SalarySlip(TransactionBase):
self.set(component_type, [ self.set(component_type, [
d for d in self.get(component_type) d for d in self.get(component_type)
if d.salary_component != component_data.salary_component if d.salary_component != component_data.salary_component
or d.additional_salary and additional_salary.name != d.additional_salary or (d.additional_salary and additional_salary.name != d.additional_salary)
or d == component_row or d == component_row
]) ])

View File

@ -312,7 +312,7 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name, create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False) payroll_period, deduct_random=False, num=6)
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
'test_ytd@salary.com'}, order_by = 'posting_date') 'test_ytd@salary.com'}, order_by = 'posting_date')

View File

@ -111,12 +111,19 @@ frappe.ui.form.on('Salary Structure', {
frappe.set_route('Form', 'Salary Structure Assignment', doc.name); frappe.set_route('Form', 'Salary Structure Assignment', doc.name);
}); });
frm.add_custom_button(__("Assign to Employees"),function () { frm.add_custom_button(__("Assign to Employees"),function () {
frm.trigger('assign_to_employees') frm.trigger('assign_to_employees')
}) })
} }
// set columns read-only
let fields_read_only = ["is_tax_applicable", "is_flexible_benefit", "variable_based_on_taxable_salary"]; let fields_read_only = ["is_tax_applicable", "is_flexible_benefit", "variable_based_on_taxable_salary"];
fields_read_only.forEach(function(field) { fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; frm.fields_dict.earnings.grid.update_docfield_property(
field, 'read_only', 1
);
frm.fields_dict.deductions.grid.update_docfield_property(
field, 'read_only', 1
);
}); });
frm.trigger('set_earning_deduction_component'); frm.trigger('set_earning_deduction_component');
}, },

View File

@ -164,7 +164,13 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.employee = employee salary_structure_assignment.employee = employee
salary_structure_assignment.base = 50000 salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000 salary_structure_assignment.variable = 5000
salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1)
if getdate(nowdate()).day == 1:
date = from_date or nowdate()
else:
date = from_date or add_days(nowdate(), -1)
salary_structure_assignment.from_date = date
salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.currency = currency salary_structure_assignment.currency = currency
salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.payroll_payable_account = get_payable_account(company)

View File

@ -145,7 +145,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-31 15:49:36.361253", "modified": "2021-03-31 22:44:46.267974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Structure Assignment", "name": "Salary Structure Assignment",

View File

@ -8,7 +8,7 @@
"is_mandatory": 1, "is_mandatory": 1,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-06-01 11:53:54.553947", "modified": "2020-06-29 11:53:54.553947",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create Payroll Period", "name": "Create Payroll Period",
"owner": "Administrator", "owner": "Administrator",

View File

@ -1,19 +1,19 @@
{ {
"action": "Go to Page", "action": "Update Settings",
"creation": "2020-06-04 16:34:29.664917", "creation": "2020-06-04 16:34:29.664917",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0, "is_mandatory": 0,
"is_single": 0, "is_single": 1,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-06-04 16:34:29.664917", "modified": "2020-06-29 16:34:29.664917",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Payroll Settings", "name": "Payroll Settings",
"owner": "Administrator", "owner": "Administrator",
"path": "#Form/Payroll Settings", "reference_document": "Payroll Settings",
"show_full_form": 0, "show_full_form": 0,
"title": "Payroll Settings", "title": "Payroll Settings",
"validate_action": 1 "validate_action": 0
} }

View File

@ -10,10 +10,12 @@ frappe.ui.form.on('Products Settings', {
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname })); ).map(df => ({ label: df.label, value: df.fieldname }));
const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname); frm.fields_dict.filter_fields.grid.update_docfield_property(
field.fieldtype = 'Select'; 'fieldname', 'fieldtype', 'Select'
field.options = valid_fields; );
frm.fields_dict.filter_fields.grid.refresh(); frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'options', valid_fields
);
}); });
} }
}); });

View File

@ -276,74 +276,3 @@ erpnext.taxes.set_conditional_mandatory_rate_or_amount = function(grid_row) {
} }
} }
} }
// For customizing print
cur_frm.pformat.total = function(doc) { return ''; }
cur_frm.pformat.discount_amount = function(doc) { return ''; }
cur_frm.pformat.grand_total = function(doc) { return ''; }
cur_frm.pformat.rounded_total = function(doc) { return ''; }
cur_frm.pformat.in_words = function(doc) { return ''; }
cur_frm.pformat.taxes= function(doc){
//function to make row of table
var make_row = function(title, val, bold, is_negative) {
var bstart = '<b>'; var bend = '</b>';
return '<tr><td style="width:50%;">' + (bold?bstart:'') + title + (bold?bend:'') + '</td>'
+ '<td style="width:50%;text-align:right;">' + (is_negative ? '- ' : '')
+ format_currency(val, doc.currency) + '</td></tr>';
}
function print_hide(fieldname) {
var doc_field = frappe.meta.get_docfield(doc.doctype, fieldname, doc.name);
return doc_field.print_hide;
}
out ='';
if (!doc.print_without_amount) {
var cl = doc.taxes || [];
// outer table
var out='<div><table class="noborder" style="width:100%"><tr><td style="width: 60%"></td><td>';
// main table
out +='<table class="noborder" style="width:100%">';
if(!print_hide('total')) {
out += make_row('Total', doc.total, 1);
}
// Discount Amount on net total
if(!print_hide('discount_amount') && doc.apply_discount_on == "Net Total" && doc.discount_amount)
out += make_row('Discount Amount', doc.discount_amount, 0, 1);
// add rows
if(cl.length){
for(var i=0;i<cl.length;i++) {
if(cl[i].tax_amount!=0 && !cl[i].included_in_print_rate)
out += make_row(cl[i].description, cl[i].tax_amount, 0);
}
}
// Discount Amount on grand total
if(!print_hide('discount_amount') && doc.apply_discount_on == "Grand Total" && doc.discount_amount)
out += make_row('Discount Amount', doc.discount_amount, 0, 1);
// grand total
if(!print_hide('grand_total'))
out += make_row('Grand Total', doc.grand_total, 1);
if(!print_hide('rounded_total'))
out += make_row('Rounded Total', doc.rounded_total, 1);
if(doc.in_words && !print_hide('in_words')) {
out +='</table></td></tr>';
out += '<tr><td colspan = "2">';
out += '<table><tr><td style="width:25%;"><b>In Words</b></td>';
out += '<td style="width:50%;">' + doc.in_words + '</td></tr>';
}
out += '</table></td></tr></table></div>';
}
return out;
}

View File

@ -0,0 +1,14 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.on('Website Theme', {
validate(frm) {
let theme_scss = frm.doc.theme_scss;
if (theme_scss && theme_scss.includes('frappe/public/scss/website')
&& !theme_scss.includes('erpnext/public/scss/website')
) {
frm.set_value('theme_scss',
`${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`);
}
}
});

View File

@ -8,6 +8,7 @@
"enable", "enable",
"section_break_2", "section_break_2",
"sandbox_mode", "sandbox_mode",
"applicable_from",
"credentials", "credentials",
"auth_token", "auth_token",
"token_expiry" "token_expiry"
@ -48,12 +49,19 @@
"fieldname": "sandbox_mode", "fieldname": "sandbox_mode",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Sandbox Mode" "label": "Sandbox Mode"
},
{
"fieldname": "applicable_from",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Applicable From",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-01-13 12:04:49.449199", "modified": "2021-03-30 12:26:25.538294",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "E Invoice Settings", "name": "E Invoice Settings",

View File

@ -5,6 +5,7 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"company",
"gstin", "gstin",
"username", "username",
"password" "password"
@ -30,12 +31,20 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Password", "label": "Password",
"reqd": 1 "reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-22 15:10:53.466205", "modified": "2021-03-22 12:16:56.365616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "E Invoice User", "name": "E Invoice User",

View File

@ -3,4 +3,17 @@ import frappe
def setup(company=None, patch=True): def setup(company=None, patch=True):
pass add_custom_roles_for_reports()
def add_custom_roles_for_reports():
"""Add Access Control to UAE VAT 201."""
if not frappe.db.get_value('Custom Role', dict(report='DATEV')):
frappe.get_doc(dict(
doctype='Custom Role',
report='DATEV',
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager')
]
)).insert()

View File

@ -69,7 +69,7 @@ state_numbers = {
"Mizoram": "15", "Mizoram": "15",
"Nagaland": "13", "Nagaland": "13",
"Odisha": "21", "Odisha": "21",
"Other Territory": "98", "Other Territory": "97",
"Pondicherry": "34", "Pondicherry": "34",
"Punjab": "03", "Punjab": "03",
"Rajasthan": "08", "Rajasthan": "08",

View File

@ -1,12 +1,13 @@
erpnext.setup_einvoice_actions = (doctype) => { erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, { frappe.ui.form.on(doctype, {
async refresh(frm) { async refresh(frm) {
const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable"); const res = await frappe.call({
const supply_type = frm.doc.gst_category; method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); args: { doc: frm.doc }
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; });
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; 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) { 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 action = () => {
const d = new frappe.ui.Dialog({ let message = __('Cancellation of e-way bill is currently not supported. ');
title: __('Cancel E-Way Bill'), message += '<br><br>';
fields: fields, 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() { primary_action: function() {
const data = d.get_values();
frappe.call({ frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { args: { doctype, docname: name },
doctype,
docname: name,
eway_bill: ewaybill,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true, freeze: true,
callback: () => frm.reload_doc() || d.hide(), callback: () => frm.reload_doc()
error: () => d.hide()
}); });
}, },
primary_action_label: __('Submit') primary_action_label: __('Yes')
}); });
d.show();
}; };
add_custom_button(__("Cancel E-Way Bill"), action); add_custom_button(__("Cancel E-Way Bill"), action);
} }

View File

@ -15,18 +15,43 @@ import traceback
import io import io
from frappe import _, bold from frappe import _, bold
from pyqrcode import create as qrcreate 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 frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply 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'] 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') company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes') 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 return
if doc.docstatus == 0 and doc._action == 'save': if doc.docstatus == 0 and doc._action == 'save':
@ -35,6 +60,8 @@ def validate_einvoice_fields(doc):
if len(doc.name) > 16: if len(doc.name) > 16:
raise_document_name_too_long_error() raise_document_name_too_long_error()
doc.einvoice_status = 'Pending'
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: 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')) 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): 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_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name invoice_name = invoice.name
@ -87,56 +117,39 @@ def get_doc_details(invoice):
invoice_date=invoice_date invoice_date=invoice_date
)) ))
def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None): def validate_address_fields(address, is_shipping_address):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] if ((not address.gstin and not is_shipping_address)
or not address.city
if ((not d.gstin and not shipping_address) or not address.pincode
or not d.city or not address.address_title
or not d.pincode or not address.address_line1
or not d.address_title or not address.gst_state_number):
or not d.address_line1
or not d.gst_state_number):
frappe.throw( frappe.throw(
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields') 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 # according to einvoice standard
pincode = 999999 addr.pincode = 999999
party_address_details = frappe._dict(dict( party_address_details = frappe._dict(dict(
legal_name=sanitize_for_json(d.address_title), legal_name=sanitize_for_json(addr.address_title),
location=sanitize_for_json(d.city), location=sanitize_for_json(addr.city),
pincode=d.pincode, pincode=addr.pincode, gstin=addr.gstin,
state_code=d.gst_state_number, state_code=addr.gst_state_number,
address_line1=sanitize_for_json(d.address_line1), address_line1=sanitize_for_json(addr.address_line1),
address_line2=sanitize_for_json(d.address_line2) address_line2=sanitize_for_json(addr.address_line2)
)) ))
if d.gstin:
party_address_details.gstin = d.gstin
return party_address_details 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): def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city = frappe.db.get_value( address_title, address_line1, address_line2, city = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] '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.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = 0
item.unit_rate = abs(item.base_net_amount / item.qty) if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
item.gross_amount = abs(item.base_net_amount) item.discount_amount = abs(item.base_amount - item.base_net_amount)
item.taxable_value = abs(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 = 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 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 is_applicable = t.tax_amount and t.account_head in gst_accounts_list
if is_applicable: if is_applicable:
# this contains item wise tax rate & tax amount (incl. discount) # 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_rate = item_tax_detail[0]
# item tax amount excluding discount amount # 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: if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1] 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']: if t.account_head in gst_accounts[f'{tax_type}_account']:
item.tax_rate += item_tax_rate item.tax_rate += item_tax_rate
item[f'{tax_type}_amount'] += abs(item_tax_amount) item[f'{tax_type}_amount'] += abs(item_tax_amount)
else:
# TODO: other charges per item
pass
return item return item
@ -232,10 +253,14 @@ def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict()) invoice_value_details = frappe._dict(dict())
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total) # Discount already applied on net total which means on items
invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount) 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: 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 # since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0 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_igst_amt = 0
invoice_value_details.total_cess_amt = 0 invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0 invoice_value_details.total_other_charges = 0
considered_rows = []
for t in invoice.taxes: 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_list:
if t.account_head in gst_accounts.cess_account: if t.account_head in gst_accounts.cess_account:
# using after discount amt since item also uses after discount amt for cess calc # 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']: for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']: 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: 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 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): def get_payment_details(invoice):
payee_name = invoice.company payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) 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): 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') invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict( return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') 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): def get_eway_bill_details(invoice):
if invoice.is_return: 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' } mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@ -307,9 +358,15 @@ def get_eway_bill_details(invoice):
def validate_mandatory_fields(invoice): def validate_mandatory_fields(invoice):
if not invoice.company_address: 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: 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'): if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw( frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), _('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') 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): def make_einvoice(invoice):
validate_mandatory_fields(invoice) validate_mandatory_fields(invoice)
@ -330,12 +420,12 @@ def make_einvoice(invoice):
item_list = get_item_list(invoice) item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice) doc_details = get_doc_details(invoice)
invoice_value_details = get_invoice_value_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': if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address) buyer_details = get_overseas_address_details(invoice.customer_address)
else: 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) place_of_supply = get_place_of_supply(invoice, invoice.doctype)
if place_of_supply: if place_of_supply:
place_of_supply = place_of_supply.split('-')[0] 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] place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
buyer_details.update(dict(place_of_supply=place_of_supply)) 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({}) 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.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
if invoice.gst_category == 'Overseas': if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name) shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else: 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: if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice) 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) 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) eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented # not yet implemented
@ -369,18 +462,70 @@ def make_einvoice(invoice):
period_details=period_details, prev_doc_details=prev_doc_details, period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details export_details=export_details, eway_bill_details=eway_bill_details
) )
einvoice = safe_json_load(einvoice)
validations = json.loads(read_json('einv_validation')) try:
errors = validate_einvoice(validations, einvoice) einvoice = safe_json_load(einvoice)
if errors: einvoice = santize_einvoice_fields(einvoice)
message = "\n".join([ validate_totals(einvoice)
"E Invoice: ", json.dumps(einvoice, indent=4),
"-" * 50, except Exception:
"Errors: ", json.dumps(errors, indent=4) log_error(einvoice)
]) link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
frappe.log_error(title="E Invoice Validation Failed", message=message) frappe.throw(
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) _('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 return einvoice
@ -396,72 +541,22 @@ def safe_json_load(json_string):
snippet = json_string[start:end] snippet = json_string[start:end]
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet)) 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): class RequestFailed(Exception):
if errors is None: pass
errors = [] class CancellationNotAllowed(Exception):
for fieldname, field_validation in validations.items(): pass
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 GSPConnector(): class GSPConnector():
def __init__(self, doctype=None, docname=None): def __init__(self, doctype=None, docname=None):
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') self.doctype = doctype
sandbox_mode = self.e_invoice_settings.sandbox_mode self.docname = docname
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.set_invoice()
self.credentials = self.get_credentials() self.set_credentials()
# authenticate url is same for sandbox & live # authenticate url is same for sandbox & live
self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' 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.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' 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.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' 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: if self.invoice:
gstin = self.get_seller_gstin() gstin = self.get_seller_gstin()
if not self.e_invoice_settings.enable: credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
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 credentials_for_gstin:
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == 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: else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
return credentials
def get_seller_gstin(self): def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') 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() self.e_invoice_settings.reload()
except Exception: except Exception:
self.log_error(res) log_error(res)
self.raise_error(True) self.raise_error(True)
def get_headers(self): def get_headers(self):
@ -551,16 +657,15 @@ class GSPConnector():
if res.get('success'): if res.get('success'):
return res.get('result') return res.get('result')
else: else:
self.log_error(res) log_error(res)
raise RequestFailed raise RequestFailed
except RequestFailed: except RequestFailed:
self.raise_error() self.raise_error()
except Exception: except Exception:
self.log_error() log_error()
self.raise_error(True) self.raise_error(True)
@staticmethod @staticmethod
def get_gstin_details(gstin): def get_gstin_details(gstin):
'''fetch and cache GSTIN details''' '''fetch and cache GSTIN details'''
@ -576,12 +681,13 @@ class GSPConnector():
return details return details
def generate_irn(self): def generate_irn(self):
headers = self.get_headers() data = {}
einvoice = make_einvoice(self.invoice)
data = json.dumps(einvoice, indent=4)
try: 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) res = self.make_request('post', self.generate_irn_url, headers, data)
if res.get('success'): if res.get('success'):
self.set_einvoice_data(res.get('result')) self.set_einvoice_data(res.get('result'))
@ -601,12 +707,36 @@ class GSPConnector():
except RequestFailed: except RequestFailed:
errors = self.sanitize_error_message(res.get('message')) errors = self.sanitize_error_message(res.get('message'))
self.set_failed_status(errors=errors)
self.raise_error(errors=errors) self.raise_error(errors=errors)
except Exception: except Exception as e:
self.log_error(data) self.set_failed_status(errors=str(e))
log_error(data)
self.raise_error(True) 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): def get_irn_details(self, irn):
headers = self.get_headers() headers = self.get_headers()
@ -623,21 +753,30 @@ class GSPConnector():
self.raise_error(errors=errors) self.raise_error(errors=errors)
except Exception: except Exception:
self.log_error() log_error()
self.raise_error(True) self.raise_error(True)
def cancel_irn(self, irn, reason, remark): def cancel_irn(self, irn, reason, remark):
headers = self.get_headers() data, res = {}, {}
data = json.dumps({
'Irn': irn,
'Cnlrsn': reason,
'Cnlrem': remark
}, indent=4)
try: 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) 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_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 = { self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype, 'doctype': self.invoice.doctype,
'docname': self.invoice.name, 'docname': self.invoice.name,
@ -650,12 +789,41 @@ class GSPConnector():
except RequestFailed: except RequestFailed:
errors = self.sanitize_error_message(res.get('message')) errors = self.sanitize_error_message(res.get('message'))
self.set_failed_status(errors=errors)
self.raise_error(errors=errors) self.raise_error(errors=errors)
except Exception: except CancellationNotAllowed as e:
self.log_error(data) 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) 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): def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs) args = frappe._dict(kwargs)
@ -694,7 +862,7 @@ class GSPConnector():
self.raise_error(errors=errors) self.raise_error(errors=errors)
except Exception: except Exception:
self.log_error(data) log_error(data)
self.raise_error(True) self.raise_error(True)
def cancel_eway_bill(self, eway_bill, reason, remark): def cancel_eway_bill(self, eway_bill, reason, remark):
@ -726,7 +894,7 @@ class GSPConnector():
self.raise_error(errors=errors) self.raise_error(errors=errors)
except Exception: except Exception:
self.log_error(data) log_error(data)
self.raise_error(True) self.raise_error(True)
def sanitize_error_message(self, message): def sanitize_error_message(self, message):
@ -741,6 +909,9 @@ class GSPConnector():
] ]
then we trim down the message by looping over errors then we trim down the message by looping over errors
''' '''
if not message:
return []
errors = re.findall(': [^:]+', message) errors = re.findall(': [^:]+', message)
for idx, e in enumerate(errors): for idx, e in enumerate(errors):
# remove colons # remove colons
@ -752,22 +923,6 @@ class GSPConnector():
return errors 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=[]): def raise_error(self, raise_exception=False, errors=[]):
title = _('E Invoice Request Failed') title = _('E Invoice Request Failed')
if errors: if errors:
@ -790,7 +945,10 @@ class GSPConnector():
self.invoice.ack_no = res.get('AckNo') self.invoice.ack_no = res.get('AckNo')
self.invoice.ack_date = res.get('AckDt') self.invoice.ack_date = res.get('AckDt')
self.invoice.signed_einvoice = dec_signed_invoice 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.signed_qr_code = res.get('SignedQRCode')
self.invoice.einvoice_status = 'Generated'
self.attach_qrcode_image() self.attach_qrcode_image()
@ -800,7 +958,6 @@ class GSPConnector():
'label': _('IRN Generated') 'label': _('IRN Generated')
} }
self.update_invoice() self.update_invoice()
def attach_qrcode_image(self): def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype doctype = self.invoice.doctype
@ -827,6 +984,17 @@ class GSPConnector():
self.invoice.flags.ignore_validate = True self.invoice.flags.ignore_validate = True
self.invoice.save() 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): def sanitize_for_json(string):
"""Escape JSON specific characters from a string.""" """Escape JSON specific characters from a string."""
@ -856,5 +1024,114 @@ def generate_eway_bill(doctype, docname, **kwargs):
@frappe.whitelist() @frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname) # TODO: uncomment when eway_bill api from Adequare is enabled
gsp_connector.cancel_eway_bill(eway_bill, reason, remark) # 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

View File

@ -12,14 +12,14 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today from frappe.utils import today
def setup(company=None, patch=True): def setup(company=None, patch=True):
setup_company_independent_fixtures() setup_company_independent_fixtures(patch=patch)
if not patch: if not patch:
make_fixtures(company) make_fixtures(company)
# TODO: for all countries # TODO: for all countries
def setup_company_independent_fixtures(): def setup_company_independent_fixtures(patch=False):
make_custom_fields() make_custom_fields()
make_property_setters() make_property_setters(patch=patch)
add_permissions() add_permissions()
add_custom_roles_for_reports() add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
@ -51,7 +51,7 @@ def create_hsn_codes(data, code_field):
def add_custom_roles_for_reports(): def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register', 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)): if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict( frappe.get_doc(dict(
@ -112,10 +112,11 @@ def add_print_formats():
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_property_setters(): def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters # GST rules do not allow for an invoice no. bigger than 16 characters
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') if not patch:
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
def make_custom_fields(update=True): def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@ -127,6 +128,9 @@ def make_custom_fields(update=True):
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', 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', fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
print_hide=1) 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 = [ purchase_invoice_gst_category = [
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
@ -156,6 +160,13 @@ def make_custom_fields(update=True):
fetch_if_empty=1), fetch_if_empty=1),
] ]
delivery_note_gst_category = [
dict(fieldname='gst_category', label='GST Category',
fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1,
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
fetch_from='customer.gst_category', fetch_if_empty=1),
]
invoice_gst_fields = [ invoice_gst_fields = [
dict(fieldname='invoice_copy', label='Invoice Copy', dict(fieldname='invoice_copy', label='Invoice Copy',
fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1,
@ -280,7 +291,7 @@ def make_custom_fields(update=True):
'allow_on_submit': 1, 'allow_on_submit': 1,
'insert_after': 'customer_name_in_arabic', 'insert_after': 'customer_name_in_arabic',
'translatable': 0, 'translatable': 0,
} }
] ]
si_ewaybill_fields = [ si_ewaybill_fields = [
@ -408,21 +419,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, 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'), 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, 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'), 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, 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'), 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='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=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='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=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)
] ]
custom_fields = { custom_fields = {
@ -438,7 +465,7 @@ def make_custom_fields(update=True):
'Purchase Order': purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Sales Order': sales_invoice_gst_fields, 'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field, 'Tax Category': inter_state_gst_field,
'Item': [ 'Item': [
@ -453,7 +480,7 @@ def make_custom_fields(update=True):
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Order 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], '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 Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt 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], 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],

View File

@ -12,14 +12,14 @@ class TestIndiaUtils(unittest.TestCase):
mock_get_cached.return_value = "India" # mock country mock_get_cached.return_value = "India" # mock country
posting_date = "2021-05-01" posting_date = "2021-05-01"
invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05", invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05",
"SI.2020.0001", "PI2021 - 001" ] "SI.2020.0001", "PI2021 - 001"]
for name in invalid_names: for name in invalid_names:
doc = frappe._dict(name=name, posting_date=posting_date) doc = frappe._dict(name=name, posting_date=posting_date)
self.assertRaises(frappe.ValidationError, validate_document_name, doc) self.assertRaises(frappe.ValidationError, validate_document_name, doc)
valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001", valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001",
"2020-PI-0001", "PI2020-0001" ] "2020-PI-0001", "PI2020-0001"]
for name in valid_names: for name in valid_names:
doc = frappe._dict(name=name, posting_date=posting_date) doc = frappe._dict(name=name, posting_date=posting_date)
try: try:

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json import frappe, re, json
from frappe import _ from frappe import _
import erpnext 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.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.accounts_controller import get_taxes_and_charges
@ -41,24 +41,25 @@ def validate_gstin_for_india(doc, method):
return return
if len(doc.gstin) != 15: 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 gst_category and gst_category == 'UIN Holders':
if not GSTIN_UIN_FORMAT.match(doc.gstin): 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: else:
if not GSTIN_FORMAT.match(doc.gstin): 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) validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc) set_gst_state_and_state_number(doc)
if not doc.gst_state: 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]: if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number)) .format(doc.gst_state_number), title=_("Invalid GSTIN"))
def validate_pan_for_india(doc, method): def validate_pan_for_india(doc, method):
if doc.get('country') != 'India' or not doc.pan: if doc.get('country') != 'India' or not doc.pan:
@ -154,6 +155,7 @@ def set_place_of_supply(doc, method=None):
def validate_document_name(doc, method=None): def validate_document_name(doc, method=None):
"""Validate GST invoice number requirements.""" """Validate GST invoice number requirements."""
country = frappe.get_cached_value("Company", doc.company, "country") country = frappe.get_cached_value("Company", doc.company, "country")
# Date was chosen as start of next FY to avoid irritating current users. # Date was chosen as start of next FY to avoid irritating current users.
@ -832,3 +834,48 @@ def get_regional_round_off_accounts(company, account_list):
account_list.extend(gst_account_list) account_list.extend(gst_account_list)
return 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

View File

@ -139,6 +139,9 @@ def make_custom_fields(update=True):
dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code',
fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1,
fetch_from="customer.fiscal_code"), fetch_from="customer.fiscal_code"),
dict(fieldname='type_of_document', label='Type of Document',
fieldtype='Select', insert_after='customer_fiscal_code',
options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'),
], ],
'Purchase Invoice Item': invoice_item_fields, 'Purchase Invoice Item': invoice_item_fields,
'Sales Order Item': invoice_item_fields, 'Sales Order Item': invoice_item_fields,

View File

@ -57,11 +57,12 @@ def prepare_invoice(invoice, progressive_number):
invoice.company_address_data = company_address invoice.company_address_data = company_address
#Set invoice type #Set invoice type
if invoice.is_return and invoice.return_against: if not invoice.type_of_document:
invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) if invoice.is_return and invoice.return_against:
invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
else: invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
invoice.type_of_document = "TD01" #Sales Invoice (Fattura) else:
invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
#set customer information #set customer information
invoice.customer_data = frappe.get_doc("Customer", invoice.customer) invoice.customer_data = frappe.get_doc("Customer", invoice.customer)

View File

@ -1,29 +1,22 @@
{ {
"add_total_row": 0, "add_total_row": 0,
"apply_user_permissions": 0, "columns": [],
"creation": "2019-04-24 08:45:16.650129", "creation": "2019-04-24 08:45:16.650129",
"disabled": 0, "disable_prepared_report": 0,
"icon": "octicon octicon-repo-pull", "disabled": 0,
"color": "#4CB944", "docstatus": 0,
"docstatus": 0, "doctype": "Report",
"doctype": "Report", "filters": [],
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"module": "Regional", "modified": "2021-04-06 12:23:00.379517",
"name": "DATEV", "modified_by": "Administrator",
"owner": "Administrator", "module": "Regional",
"ref_doctype": "GL Entry", "name": "DATEV",
"report_name": "DATEV", "owner": "Administrator",
"report_type": "Script Report", "prepared_report": 0,
"roles": [ "ref_doctype": "GL Entry",
{ "report_name": "DATEV",
"role": "Accounts User" "report_type": "Script Report",
}, "roles": []
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
} }

View File

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

View File

@ -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"
}
]
}

View 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
}
]

View File

@ -199,7 +199,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict()
items = frappe.db.sql(""" items = frappe.db.sql("""
select item_code, parent, base_net_amount, item_tax_rate select item_code, parent, taxable_value, item_tax_rate
from `tab%s Item` from `tab%s Item`
where parent in (%s) where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@ -207,7 +207,7 @@ class Gstr1Report(object):
for d in items: for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}): if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum(i.get('base_net_amount', 0) for i in items sum(i.get('taxable_value', 0) for i in items
if i.item_code == d.item_code and i.parent == d.parent)) if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {} item_tax_rate = {}

View File

@ -212,7 +212,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Represents Company", "label": "Represents Company",
"options": "Company" "options": "Company",
"unique": 1
}, },
{ {
"depends_on": "represents_company", "depends_on": "represents_company",

View File

@ -279,11 +279,6 @@ erpnext.PointOfSale.Controller = class {
const item_row = frappe.model.get_doc(cdt, cdn); const item_row = frappe.model.get_doc(cdt, cdn);
if (item_row && item_row[fieldname] != value) { 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 { item_code, batch_no, uom } = this.item_details.current_item;
const event = { const event = {
field: fieldname, field: fieldname,

Some files were not shown because too many files have changed in this diff Show More