Merge pull request #25195 from ankush/use_isinstance_instead_of_type
fix: broken type check in stock ageing report
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 49b2b18..059e1d3 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -42,10 +42,9 @@
});
});
- frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field",
- frm.doc.name).options = options;
-
- frm.fields_dict.bank_transaction_mapping.grid.refresh();
+ frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
+ 'bank_transaction_field', 'options', options
+ );
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 17e39d5..ce149f9 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -61,7 +61,6 @@
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
- print(linked_payments)
self.assertTrue(linked_payments[0][3])
# Check error if already reconciled
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 03c3eb0..f96f591 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -293,6 +293,11 @@
accounts_dict = {}
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
+ if not hasattr(account, "parent_account"):
+ msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
+ msg += "<br><br>"
+ msg += _("Alternatively, you can download the template and fill your data in.")
+ frappe.throw(msg, title=_("Parent Account Missing"))
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 37b03f3..d76641d 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -327,18 +327,16 @@
},
setup_balance_formatter: function() {
- var me = this;
- $.each(["balance", "party_balance"], function(i, field) {
- var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name);
- df.formatter = function(value, df, options, doc) {
- var currency = frappe.meta.get_field_currency(df, doc);
- var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
- return "<div style='text-align: right'>"
- + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
- + " " + dr_or_cr
- + "</div>";
- }
- })
+ const formatter = function(value, df, options, doc) {
+ var currency = frappe.meta.get_field_currency(df, doc);
+ var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
+ return "<div style='text-align: right'>"
+ + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
+ + " " + dr_or_cr
+ + "</div>";
+ };
+ this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
+ this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
},
reference_name: function(doc, cdt, cdn) {
@@ -431,15 +429,6 @@
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", {
party: function(frm, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn);
@@ -511,8 +500,11 @@
};
$.each(field_label_map, function (fieldname, label) {
- var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name);
- df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label;
+ frm.fields_dict.accounts.grid.update_docfield_property(
+ fieldname,
+ 'label',
+ frm.doc.multi_currency ? (label + " in Account Currency") : label
+ );
})
},
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 6b07197..08103184 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -234,8 +234,9 @@
});
if (invoices) {
- frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number",
- me.frm.doc.name).options = "\n" + invoices.join("\n");
+ this.frm.fields_dict.payment.grid.update_docfield_property(
+ 'invoice_number', 'options', "\n" + invoices.join("\n")
+ );
$.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 40f77b4..6d2cffc 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -12,6 +12,7 @@
from frappe.model.mapper import map_doc, map_child_doc
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.core.page.background_jobs.background_jobs import get_info
+import json
from six import iteritems
@@ -78,8 +79,11 @@
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
+ sales_invoice.set_posting_time = 1
+ sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save()
sales_invoice.submit()
+
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
@@ -91,10 +95,13 @@
credit_note = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1
+ credit_note.set_posting_time = 1
+ credit_note.posting_date = getdate(self.posting_date)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
+
self.consolidated_credit_note = credit_note.name
return credit_note.name
@@ -131,12 +138,14 @@
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
+ update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = 'Actual'
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
+ tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax)
for payment in doc.get('payments'):
@@ -168,11 +177,9 @@
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
sales_invoice.is_pos = 1
- # date can be pos closing date?
- sales_invoice.posting_date = getdate(nowdate())
return sales_invoice
-
+
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs:
doc.load_from_db()
@@ -187,6 +194,26 @@
si.flags.ignore_validate = True
si.cancel()
+def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
+ consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
+ tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
+
+ if not consolidated_tax_detail:
+ consolidated_tax_detail = {}
+
+ for item_code, tax_data in tax_row_detail.items():
+ if consolidated_tax_detail.get(item_code):
+ consolidated_tax_data = consolidated_tax_detail.get(item_code)
+ consolidated_tax_detail.update({
+ item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
+ })
+ else:
+ consolidated_tax_detail.update({
+ item_code: [tax_data[0], tax_data[1]]
+ })
+
+ consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
+
def get_all_unconsolidated_invoices():
filters = {
'consolidated_invoice': [ 'in', [ '', None ]],
@@ -214,7 +241,7 @@
if len(invoices) >= 5 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
- enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
+ enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
create_merge_logs(invoice_by_customer, closing_entry)
@@ -227,21 +254,21 @@
if len(merge_logs) >= 5:
closing_entry.set_status(update=True, status='Queued')
- enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
+ enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
- merge_log.posting_date = getdate(nowdate())
+ merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
-
+
if closing_entry:
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
@@ -256,7 +283,7 @@
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
-def enqueue_job(job, invoice_by_customer, closing_entry):
+def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
check_scheduler_status()
job_name = closing_entry.get("name")
@@ -269,6 +296,7 @@
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
+ merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test
)
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index d880caa..040a815 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -5,6 +5,7 @@
import frappe
import unittest
+import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
@@ -99,4 +100,51 @@
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
+ def test_consolidated_invoice_item_taxes(self):
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
+ try:
+ inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
+
+ inv.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 9
+ })
+ inv.insert()
+ inv.submit()
+
+ inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
+ inv2.get('items')[0].item_code = '_Test Item 2'
+ inv2.append("taxes", {
+ "account_head": "_Test Account VAT - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "VAT",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 5
+ })
+ inv2.insert()
+ inv2.submit()
+
+ consolidate_pos_invoices()
+ inv.load_from_db()
+
+ consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
+ item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
+
+ tax_rate, amount = item_wise_tax_detail.get('_Test Item')
+ self.assertEqual(tax_rate, 9)
+ self.assertEqual(amount, 9)
+
+ tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
+ self.assertEqual(tax_rate2, 5)
+ self.assertEqual(amount2, 5)
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js
index 8890d59..3625393 100644
--- a/erpnext/accounts/doctype/pos_settings/pos_settings.js
+++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js
@@ -16,8 +16,11 @@
}
});
- 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)
+ );
});
+
}
});
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index f28cee7..ef9aad5 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -328,6 +328,21 @@
self.assertEquals(item.discount_amount, 110)
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):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = {
@@ -560,6 +575,7 @@
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
+ "discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index c676abd..b91a7a5 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -471,7 +471,7 @@
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}")
.format(doc.name))
else:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 66a8e20..e61cde8 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -496,15 +496,6 @@
}
}
-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", {
setup: function(frm) {
frm.custom_make_buttons = {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 18b6637..739bd67 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -127,7 +127,6 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
- "adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@@ -1327,13 +1326,6 @@
"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",
"description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
@@ -1378,7 +1370,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-03-09 21:12:30.422084",
+ "modified": "2021-03-30 21:45:58.334107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
index 3e1c522..ada665a 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
@@ -1,14 +1,14 @@
var globalOnload = frappe.listview_settings['Sales Invoice'].onload;
-frappe.listview_settings['Sales Invoice'].onload = function (doclist) {
+frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
// Provision in case onload event is added to sales_invoice.js in future
if (globalOnload) {
- globalOnload(doclist);
+ globalOnload(list_view);
}
const action = () => {
- const selected_docs = doclist.get_checked_items();
- const docnames = doclist.get_checked_items(true);
+ const selected_docs = list_view.get_checked_items();
+ const docnames = list_view.get_checked_items(true);
for (let doc of selected_docs) {
if (doc.docstatus !== 1) {
@@ -19,7 +19,7 @@
frappe.call({
method: 'erpnext.regional.india.utils.generate_ewb_json',
args: {
- 'dt': doclist.doctype,
+ 'dt': list_view.doctype,
'dn': docnames
},
callback: function(r) {
@@ -35,5 +35,140 @@
});
};
- 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'
+ });
+ }
+ });
};
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b361c0c..8a42d9e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -1,9 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-// print heading
-cur_frm.pformat.print_heading = 'Invoice';
-
{% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts");
@@ -916,7 +913,7 @@
},
callback: function(r, rt) {
if(r.message){
- data = r.message;
+ let data = r.message;
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 89a05b1..5a64e27 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -406,9 +406,10 @@
throw(_("""Payment Entry has been modified after you pulled it. Please pull it again."""))
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:
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"))
def update_reference_in_journal_entry(d, jv_obj):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 36d399c..33fbf1c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -659,6 +659,7 @@
'dr_or_cr': dr_or_cr,
'unadjusted_amount': flt(d.advance_amount),
'allocated_amount': flt(d.allocated_amount),
+ 'precision': d.precision('advance_amount'),
'exchange_rate': (self.conversion_rate
if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index f352bae..2049957 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -406,8 +406,7 @@
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
- if d.conversion_factor:
- d.stock_uom_rate = d.rate / d.conversion_factor
+ d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 5f73c55..7653a5d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -147,7 +147,9 @@
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc)
- tax.item_wise_tax_detail = {}
+ if not self.doc.get('is_consolidated'):
+ tax.item_wise_tax_detail = {}
+
tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
@@ -338,7 +340,9 @@
current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
- self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
+
+ if not self.doc.get("is_consolidated"):
+ self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
@@ -440,8 +444,9 @@
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
- for tax in self.doc.get("taxes"):
- tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
+ if not self.doc.get('is_consolidated'):
+ for tax in self.doc.get("taxes"):
+ tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self):
if self.doc.additional_discount_percentage:
diff --git a/erpnext/selling/doctype/lead_source/__init__.py b/erpnext/crm/doctype/lead_source/__init__.py
similarity index 100%
rename from erpnext/selling/doctype/lead_source/__init__.py
rename to erpnext/crm/doctype/lead_source/__init__.py
diff --git a/erpnext/crm/doctype/lead_source/lead_source.js b/erpnext/crm/doctype/lead_source/lead_source.js
new file mode 100644
index 0000000..3cbe649
--- /dev/null
+++ b/erpnext/crm/doctype/lead_source/lead_source.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Lead Source', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/lead_source/lead_source.json b/erpnext/crm/doctype/lead_source/lead_source.json
new file mode 100644
index 0000000..723c6d9
--- /dev/null
+++ b/erpnext/crm/doctype/lead_source/lead_source.json
@@ -0,0 +1,62 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:source_name",
+ "creation": "2016-09-16 01:47:47.382372",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "source_name",
+ "details"
+ ],
+ "fields": [
+ {
+ "fieldname": "source_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Source Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "details",
+ "fieldtype": "Text Editor",
+ "label": "Details"
+ }
+ ],
+ "links": [],
+ "modified": "2021-02-08 12:51:48.971517",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Lead Source",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/lead_source/lead_source.py b/erpnext/crm/doctype/lead_source/lead_source.py
similarity index 71%
rename from erpnext/selling/doctype/lead_source/lead_source.py
rename to erpnext/crm/doctype/lead_source/lead_source.py
index d2d7558..5c64fb8 100644
--- a/erpnext/selling/doctype/lead_source/lead_source.py
+++ b/erpnext/crm/doctype/lead_source/lead_source.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe
+# import frappe
from frappe.model.document import Document
class LeadSource(Document):
diff --git a/erpnext/crm/doctype/lead_source/test_lead_source.py b/erpnext/crm/doctype/lead_source/test_lead_source.py
new file mode 100644
index 0000000..b5bc649
--- /dev/null
+++ b/erpnext/crm/doctype/lead_source/test_lead_source.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestLeadSource(unittest.TestCase):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js
index c7074e8..f28d32c 100644
--- a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js
+++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js
@@ -39,11 +39,13 @@
},
set_score_range: function(frm) {
- let options = [];
+ let options = [''];
for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; 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) {
@@ -83,4 +85,4 @@
score: function(frm, cdt, cdn) {
frm.events.calculate_total_score(frm, cdt, cdn);
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
index d1f72d6..42e231d 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js
@@ -58,8 +58,12 @@
}
if (frm.doc.therapy_plan_template) {
- frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1;
- frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1;
+ frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
+ 'therapy_type', 'read_only', 1
+ );
+ frm.fields_dict.therapy_plan_details.grid.update_docfield_property(
+ 'no_of_sessions', 'read_only', 1
+ );
}
},
@@ -126,4 +130,4 @@
frm.set_value('total_sessions', total);
refresh_field('total_sessions');
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c2798a3..98d5966 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -260,7 +260,11 @@
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
- "on_trash": "erpnext.regional.check_deletion_permission"
+ "on_trash": "erpnext.regional.check_deletion_permission",
+ "validate": [
+ "erpnext.regional.india.utils.validate_document_name",
+ "erpnext.regional.india.utils.update_taxable_values"
+ ]
},
"Purchase Invoice": {
"validate": [
@@ -282,9 +286,6 @@
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
- ('Sales Invoice', 'Purchase Invoice'): {
- 'validate': ['erpnext.regional.india.utils.validate_document_name']
- },
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 7a9727f..aa5a67f 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import date_diff, add_days, getdate, cint
+from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \
get_holidays_for_employee, create_additional_leave_ledger_entry
@@ -40,7 +40,12 @@
def validate_holidays(self):
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
- frappe.throw(_("Compensatory leave request days not in valid holidays"))
+ if date_diff(self.work_end_date, self.work_from_date):
+ msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
+ else:
+ msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date)))
+
+ frappe.throw(msg)
def on_submit(self):
company = frappe.db.get_value("Employee", self.employee, "company")
@@ -63,7 +68,7 @@
leave_allocation = self.create_leave_allocation(leave_period, date_difference)
self.leave_allocation=leave_allocation.name
else:
- frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date))
+ frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date)))
def on_cancel(self):
if self.leave_allocation:
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 629bc57..ed7d588 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -80,6 +80,7 @@
self.update_user()
self.update_user_permissions()
self.reset_employee_emails_cache()
+ self.update_approver_role()
def update_user_permissions(self):
if not self.create_user_permission: return
@@ -145,6 +146,17 @@
user.save()
+ def update_approver_role(self):
+ if self.leave_approver:
+ user = frappe.get_doc("User", self.leave_approver)
+ user.flags.ignore_permissions = True
+ user.add_roles("Leave Approver")
+
+ if self.expense_approver:
+ user = frappe.get_doc("User", self.expense_approver)
+ user.flags.ignore_permissions = True
+ user.add_roles("Expense Approver")
+
def validate_date(self):
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
throw(_("Date of Birth cannot be greater than today."))
@@ -503,7 +515,7 @@
})
def has_upload_permission(doc, ptype='read', user=None):
- if not user:
+ if not user:
user = frappe.session.user
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
return True
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index e7bb6dc..5010fc3 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document
-from erpnext.hr.utils import set_employee_name
+from erpnext.hr.utils import set_employee_name, share_doc_with_approver
from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -53,6 +53,9 @@
elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected'
+ def on_update(self):
+ share_doc_with_approver(self, self.expense_approver)
+
def set_payable_account(self):
if not self.payable_account and not self.is_paid:
self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account')
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index f9e3a44..3f22ca2 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -95,12 +95,12 @@
def test_rejected_expense_claim(self):
payable_account = get_payable_account(company_name)
expense_claim = frappe.get_doc({
- "doctype": "Expense Claim",
- "employee": "_T-Employee-00001",
- "payable_account": payable_account,
- "approval_status": "Rejected",
- "expenses":
- [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
+ "doctype": "Expense Claim",
+ "employee": "_T-Employee-00001",
+ "payable_account": payable_account,
+ "approval_status": "Rejected",
+ "expenses":
+ [{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
})
expense_claim.submit()
@@ -110,6 +110,34 @@
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name})
self.assertEquals(len(gl_entry), 0)
+ def test_expense_approver_perms(self):
+ user = "test_approver_perm_emp@example.com"
+ make_employee(user, "_Test Company")
+
+ # check doc shared
+ payable_account = get_payable_account("_Test Company")
+ expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
+ expense_claim.expense_approver = user
+ expense_claim.save()
+ self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user))
+
+ # check shared doc revoked
+ expense_claim.reload()
+ expense_claim.expense_approver = "test@example.com"
+ expense_claim.save()
+ self.assertTrue(expense_claim.name not in frappe.share.get_shared("Expense Claim", user))
+
+ expense_claim.reload()
+ expense_claim.expense_approver = user
+ expense_claim.save()
+
+ frappe.set_user(user)
+ expense_claim.reload()
+ expense_claim.status = "Approved"
+ expense_claim.submit()
+ frappe.set_user("Administrator")
+
+
def get_payable_account(company):
return frappe.get_cached_value('Company', company, 'default_payable_account')
@@ -133,21 +161,21 @@
currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
expense_claim = {
- "doctype": "Expense Claim",
- "employee": employee,
- "payable_account": payable_account,
- "approval_status": "Approved",
- "company": company,
- 'currency': currency,
- "expenses": [{
+ "doctype": "Expense Claim",
+ "employee": employee,
+ "payable_account": payable_account,
+ "approval_status": "Approved",
+ "company": company,
+ "currency": currency,
+ "expenses": [{
"expense_type": "Travel",
"default_account": account,
"currency": currency,
"amount": amount,
"sanctioned_amount": sanctioned_amount,
"cost_center": cost_center
- }]
- }
+ }]
+ }
if taxes:
expense_claim.update(taxes)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 350cead..0bf551e 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
comma_or, get_fullname, add_days, nowdate, get_datetime_str
-from erpnext.hr.utils import set_employee_name, get_leave_period
+from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@@ -43,6 +43,8 @@
if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
self.notify_leave_approver()
+ share_doc_with_approver(self, self.leave_approver)
+
def on_submit(self):
if self.status == "Open":
frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted"))
@@ -417,6 +419,7 @@
))
create_leave_ledger_entry(self, args, submit)
+
def get_allocation_expiry(employee, leave_type, to_date, from_date):
''' Returns expiry of carry forward allocation in leave ledger entry '''
expiry = frappe.get_all("Leave Ledger Entry",
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 48bfa0c..b54c971 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -11,6 +11,7 @@
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
+from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@@ -567,6 +568,48 @@
self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0)
+ def test_leave_approver_perms(self):
+ employee = get_employee()
+ user = "test_approver_perm_emp@example.com"
+ make_employee(user, "_Test Company")
+
+ # set approver for employee
+ employee.reload()
+ employee.leave_approver = user
+ employee.save()
+ self.assertTrue("Leave Approver" in frappe.get_roles(user))
+
+ make_allocation_record(employee.name)
+
+ application = self.get_application(_test_records[0])
+ application.from_date = '2018-01-01'
+ application.to_date = '2018-01-03'
+ application.leave_approver = user
+ application.insert()
+ self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user))
+
+ # check shared doc revoked
+ application.reload()
+ application.leave_approver = "test@example.com"
+ application.save()
+ self.assertTrue(application.name not in frappe.share.get_shared("Leave Application", user))
+
+ application.reload()
+ application.leave_approver = user
+ application.save()
+
+ frappe.set_user(user)
+ application.reload()
+ application.status = "Approved"
+ application.submit()
+
+ # unset leave approver
+ frappe.set_user("Administrator")
+ employee.reload()
+ employee.leave_approver = ""
+ employee.save()
+
+
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation
leave_allocation = create_leave_allocation(
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 66dced4..cf13036 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -34,8 +34,8 @@
""", (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date))
if leave_application_records:
- frappe.throw(_("Leave allocation %s is linked with leave application %s"
- % (ledger.transaction_name, ', '.join(leave_application_records))))
+ frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format(
+ ledger.transaction_name, ', '.join(leave_application_records)))
def create_leave_ledger_entry(ref_doc, args, submit=True):
ledger = frappe._dict(
diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index 473193d..177c45e 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -7,6 +7,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import formatdate, getdate
+from erpnext.hr.utils import share_doc_with_approver
class OverlapError(frappe.ValidationError): pass
@@ -17,6 +18,9 @@
self.validate_approver()
self.validate_default_shift()
+ def on_update(self):
+ share_doc_with_approver(self, self.approver)
+
def on_submit(self):
if self.status not in ["Approved", "Rejected"]:
frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted"))
@@ -29,6 +33,7 @@
if self.to_date:
assignment_doc.end_date = self.to_date
assignment_doc.shift_request = self.name
+ assignment_doc.flags.ignore_permissions = 1
assignment_doc.insert()
assignment_doc.submit()
diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py
index 230bb2b..9c0d8e3 100644
--- a/erpnext/hr/doctype/shift_request/test_shift_request.py
+++ b/erpnext/hr/doctype/shift_request/test_shift_request.py
@@ -6,6 +6,7 @@
import frappe
import unittest
from frappe.utils import nowdate, add_days
+from erpnext.hr.doctype.employee.test_employee import make_employee
test_dependencies = ["Shift Type"]
@@ -19,19 +20,8 @@
set_shift_approver(department)
approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
- shift_request = frappe.get_doc({
- "doctype": "Shift Request",
- "shift_type": "Day Shift",
- "company": "_Test Company",
- "employee": "_T-Employee-00001",
- "employee_name": "_Test Employee",
- "from_date": nowdate(),
- "to_date": add_days(nowdate(), 10),
- "approver": approver,
- "status": "Approved"
- })
- shift_request.insert()
- shift_request.submit()
+ shift_request = make_shift_request(approver)
+
shift_assignments = frappe.db.sql('''
SELECT shift_request, employee
FROM `tabShift Assignment`
@@ -44,8 +34,65 @@
shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')})
self.assertEqual(shift_assignment_doc.docstatus, 2)
+ def test_shift_request_approver_perms(self):
+ employee = frappe.get_doc("Employee", "_T-Employee-00001")
+ user = "test_approver_perm_emp@example.com"
+ make_employee(user, "_Test Company")
+
+ # set approver for employee
+ employee.reload()
+ employee.shift_request_approver = user
+ employee.save()
+
+ shift_request = make_shift_request(user, do_not_submit=True)
+ self.assertTrue(shift_request.name in frappe.share.get_shared("Shift Request", user))
+
+ # check shared doc revoked
+ shift_request.reload()
+ department = frappe.get_value("Employee", "_T-Employee-00001", "department")
+ set_shift_approver(department)
+ department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
+ shift_request.approver = department_approver
+ shift_request.save()
+ self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user))
+
+ shift_request.reload()
+ shift_request.approver = user
+ shift_request.save()
+
+ frappe.set_user(user)
+ shift_request.reload()
+ shift_request.status = "Approved"
+ shift_request.submit()
+
+ # unset approver
+ frappe.set_user("Administrator")
+ employee.reload()
+ employee.shift_request_approver = ""
+ employee.save()
+
+
def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save()
department_doc.reload()
+
+def make_shift_request(approver, do_not_submit=0):
+ shift_request = frappe.get_doc({
+ "doctype": "Shift Request",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "employee_name": "_Test Employee",
+ "from_date": nowdate(),
+ "to_date": add_days(nowdate(), 10),
+ "approver": approver,
+ "status": "Approved"
+ }).insert()
+
+ if do_not_submit:
+ return shift_request
+
+ shift_request.submit()
+ return shift_request
\ No newline at end of file
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 0c4c1ca..190eb4f 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -504,3 +504,25 @@
lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
for assignment in lpa:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
+
+def share_doc_with_approver(doc, user):
+ # if approver does not have permissions, share
+ if not frappe.has_permission(doc=doc, ptype="submit", user=user):
+ frappe.share.add(doc.doctype, doc.name, user, submit=1,
+ flags={"ignore_share_permission": True})
+
+ frappe.msgprint(_("Shared with the user {0} with {1} access").format(
+ user, frappe.bold("submit"), alert=True))
+
+ # remove shared doc if approver changes
+ doc_before_save = doc.get_doc_before_save()
+ if doc_before_save:
+ approvers = {
+ "Leave Application": "leave_approver",
+ "Expense Claim": "expense_approver",
+ "Shift Request": "approver"
+ }
+
+ approver = approvers.get(doc.doctype)
+ if doc_before_save.get(approver) != doc.get(approver):
+ frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index f650b24..f4b56a0 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -15,6 +15,7 @@
"hide_custom": 0,
"icon": "hr",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "HR",
"links": [
@@ -227,41 +228,11 @@
"type": "Card Break"
},
{
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Leave Application",
- "link_to": "Leave Application",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Leave Allocation",
- "link_to": "Leave Allocation",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Leave Type",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Leave Policy",
- "link_to": "Leave Policy",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Leave Period",
- "link_to": "Leave Period",
+ "label": "Holiday List",
+ "link_to": "Holiday List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -280,8 +251,28 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Holiday List",
- "link_to": "Holiday List",
+ "label": "Leave Period",
+ "link_to": "Leave Period",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Type",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Policy",
+ "link_to": "Leave Policy",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Policy",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Policy Assignment",
+ "link_to": "Leave Policy Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -290,8 +281,18 @@
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
- "label": "Compensatory Leave Request",
- "link_to": "Compensatory Leave Request",
+ "label": "Leave Application",
+ "link_to": "Leave Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Allocation",
+ "link_to": "Leave Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -317,12 +318,12 @@
"type": "Link"
},
{
- "dependencies": "Leave Application",
+ "dependencies": "Employee",
"hidden": 0,
- "is_query_report": 1,
- "label": "Employee Leave Balance",
- "link_to": "Employee Leave Balance",
- "link_type": "Report",
+ "is_query_report": 0,
+ "label": "Compensatory Leave Request",
+ "link_to": "Compensatory Leave Request",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
@@ -384,16 +385,6 @@
"type": "Link"
},
{
- "dependencies": "Attendance",
- "hidden": 0,
- "is_query_report": 1,
- "label": "Monthly Attendance Sheet",
- "link_to": "Monthly Attendance Sheet",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
@@ -423,6 +414,15 @@
{
"hidden": 0,
"is_query_report": 0,
+ "label": "Travel Request",
+ "link_to": "Travel Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
"label": "Settings",
"onboard": 0,
"type": "Card Break"
@@ -465,6 +465,15 @@
"type": "Card Break"
},
{
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Driver",
+ "link_to": "Driver",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
@@ -544,6 +553,24 @@
{
"hidden": 0,
"is_query_report": 0,
+ "label": "Appointment Letter",
+ "link_to": "Appointment Letter",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appointment Letter Template",
+ "link_to": "Appointment Letter Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
"label": "Loans",
"onboard": 0,
"type": "Card Break"
@@ -628,33 +655,6 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Reports",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 1,
- "label": "Employee Birthday",
- "link_to": "Employee Birthday",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 1,
- "label": "Employees working on a holiday",
- "link_to": "Employees working on a holiday",
- "link_type": "Report",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
"label": "Performance",
"onboard": 0,
"type": "Card Break"
@@ -702,7 +702,74 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Employee Tax and Benefits",
+ "label": "Key Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Monthly Attendance Sheet",
+ "link_to": "Monthly Attendance Sheet",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Staffing Plan",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Recruitment Analytics",
+ "link_to": "Recruitment Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Analytics",
+ "link_to": "Employee Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Leave Balance",
+ "link_to": "Employee Leave Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Leave Balance Summary",
+ "link_to": "Employee Leave Balance Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee Advance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Advance Summary",
+ "link_to": "Employee Advance Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Other Reports",
"onboard": 0,
"type": "Card Break"
},
@@ -710,74 +777,44 @@
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
- "label": "Employee Tax Exemption Declaration",
- "link_to": "Employee Tax Exemption Declaration",
- "link_type": "DocType",
+ "label": "Employee Information",
+ "link_to": "Employee Information",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
- "is_query_report": 0,
- "label": "Employee Tax Exemption Proof Submission",
- "link_to": "Employee Tax Exemption Proof Submission",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Employee, Payroll Period",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Employee Other Income",
- "link_to": "Employee Other Income",
- "link_type": "DocType",
+ "is_query_report": 1,
+ "label": "Employee Birthday",
+ "link_to": "Employee Birthday",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
- "is_query_report": 0,
- "label": "Employee Benefit Application",
- "link_to": "Employee Benefit Application",
- "link_type": "DocType",
+ "is_query_report": 1,
+ "label": "Employees Working on a Holiday",
+ "link_to": "Employees working on a holiday",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
- "dependencies": "Employee",
+ "dependencies": "Daily Work Summary",
"hidden": 0,
- "is_query_report": 0,
- "label": "Employee Benefit Claim",
- "link_to": "Employee Benefit Claim",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Employee Tax Exemption Category",
- "link_to": "Employee Tax Exemption Category",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "Employee",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Employee Tax Exemption Sub Category",
- "link_to": "Employee Tax Exemption Sub Category",
- "link_type": "DocType",
+ "is_query_report": 1,
+ "label": "Daily Work Summary Replies",
+ "link_to": "Daily Work Summary Replies",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2021-01-21 13:38:38.941001",
+ "modified": "2021-03-24 17:35:21.483297",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index acf09f5..4f8ceb0 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -23,6 +23,7 @@
"rate_of_interest",
"is_secured_loan",
"disbursement_date",
+ "closure_date",
"disbursed_amount",
"column_break_11",
"maximum_loan_amount",
@@ -348,12 +349,18 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fieldname": "closure_date",
+ "fieldtype": "Date",
+ "label": "Closure Date",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-24 12:27:23.208240",
+ "modified": "2021-04-10 09:28:21.946972",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 4b9a894..6f8da31 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -523,33 +523,7 @@
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
def test_penalty(self):
- pledge = [{
- "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()
-
+ loan, amounts = create_loan_scenario_for_penalty(self)
# 30 days - grace period
penalty_days = 30 - 4
penalty_applicable_amount = flt(amounts['interest_amount']/2)
@@ -559,8 +533,28 @@
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
+ self.assertEquals(loan.loan_amount, 1000000)
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):
pledge = [{
"loan_security": "Test Security 1",
@@ -651,6 +645,32 @@
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
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():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index cd5df4d..662c626 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -20,6 +20,10 @@
"cost_center",
"customer_details_section",
"bank_account",
+ "disbursement_references_section",
+ "reference_date",
+ "column_break_17",
+ "reference_number",
"amended_from"
],
"fields": [
@@ -126,12 +130,31 @@
{
"fieldname": "column_break_8",
"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,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-06 10:04:30.882322",
+ "modified": "2021-04-10 10:03:41.502210",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 2b5df4b..8fbf233 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -21,6 +21,7 @@
"interest_payable",
"payable_amount",
"column_break_9",
+ "shortfall_amount",
"payable_principal_amount",
"penalty_amount",
"amount_paid",
@@ -31,6 +32,7 @@
"column_break_21",
"reference_date",
"principal_amount_paid",
+ "total_penalty_paid",
"total_interest_paid",
"repayment_details",
"amended_from"
@@ -226,12 +228,27 @@
"fieldtype": "Percent",
"label": "Rate Of Interest",
"read_only": 1
+ },
+ {
+ "fieldname": "shortfall_amount",
+ "fieldtype": "Currency",
+ "label": "Shortfall Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_penalty_paid",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Total Penalty Paid",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-05 10:06:58.792841",
+ "modified": "2021-04-10 10:00:31.859076",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index bac06c4..728eadf 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -21,6 +21,7 @@
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts)
+ self.check_future_entries()
self.validate_amount()
self.allocate_amounts(amounts)
@@ -60,19 +61,28 @@
if not self.payable_amount:
self.payable_amount = flt(amounts['payable_amount'], precision)
+ shortfall_amount = flt(frappe.db.get_value('Loan Security Shortfall', {'loan': self.against_loan, 'status': 'Pending'},
+ 'shortfall_amount'))
+
+ if shortfall_amount:
+ self.shortfall_amount = shortfall_amount
+
if amounts.get('due_date'):
self.due_date = amounts.get('due_date')
+ def check_future_entries(self):
+ future_repayment_date = frappe.db.get_value("Loan Repayment", {"posting_date": (">", self.posting_date),
+ "docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
+
+ if future_repayment_date:
+ frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date)))
+
def validate_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero"))
- if 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):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if self.total_interest_paid > self.interest_payable:
@@ -148,11 +158,28 @@
def allocate_amounts(self, repayment_details):
self.set('repayment_details', [])
self.principal_amount_paid = 0
- total_interest_paid = 0
- interest_paid = self.amount_paid - self.penalty_amount
+ self.total_penalty_paid = 0
+ interest_paid = self.amount_paid
- if self.amount_paid - self.penalty_amount > 0:
- interest_paid = self.amount_paid - self.penalty_amount
+ if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
+ self.principal_amount_paid = self.shortfall_amount
+ elif self.shortfall_amount:
+ self.principal_amount_paid = self.amount_paid
+
+ interest_paid -= self.principal_amount_paid
+
+ if interest_paid > 0:
+ if self.penalty_amount and interest_paid > self.penalty_amount:
+ self.total_penalty_paid = self.penalty_amount
+ elif self.penalty_amount:
+ self.total_penalty_paid = interest_paid
+
+ interest_paid -= self.total_penalty_paid
+
+ total_interest_paid = 0
+ # interest_paid = self.amount_paid - self.principal_amount_paid - self.penalty_amount
+
+ if interest_paid > 0:
for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
@@ -177,7 +204,7 @@
'paid_principal_amount': paid_principal
})
- if repayment_details['unaccrued_interest'] and interest_paid:
+ if repayment_details['unaccrued_interest'] and interest_paid > 0:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
@@ -193,20 +220,28 @@
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
- if interest_paid:
+ if interest_paid > 0:
self.principal_amount_paid += interest_paid
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
- if self.penalty_amount:
+ 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:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
- "debit": self.penalty_amount,
- "debit_in_account_currency": self.penalty_amount,
+ "debit": self.total_penalty_paid,
+ "debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
@@ -221,8 +256,8 @@
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.payment_account,
- "credit": self.penalty_amount,
- "credit_in_account_currency": self.penalty_amount,
+ "credit": self.total_penalty_paid,
+ "credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
@@ -240,7 +275,7 @@
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Repayment against Loan: ") + self.against_loan,
+ "remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@@ -256,7 +291,7 @@
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Repayment against Loan: ") + self.against_loan,
+ "remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
@@ -284,7 +319,9 @@
return lr
-def get_accrued_interest_entries(against_loan):
+def get_accrued_interest_entries(against_loan, posting_date=None):
+ if not posting_date:
+ posting_date = getdate()
unpaid_accrued_entries = frappe.db.sql(
"""
@@ -295,15 +332,28 @@
`tabLoan Interest Accrual`
WHERE
loan = %s
+ AND posting_date <= %s
AND (interest_amount - paid_interest_amount > 0 OR
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
ORDER BY posting_date
- """, (against_loan), as_dict=1)
+ """, (against_loan, posting_date), as_dict=1)
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
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
@@ -312,8 +362,9 @@
against_loan_doc = frappe.get_doc("Loan", against_loan)
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
- accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name)
+ 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 = {}
total_pending_interest = 0
@@ -328,8 +379,13 @@
# and if no_of_late days are positive then penalty is levied
due_date = add_days(entry.posting_date, 1)
- no_of_late_days = date_diff(posting_date,
- add_days(due_date, loan_type_details.grace_period_in_days)) + 1
+ due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days)
+
+ # 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':
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
@@ -367,7 +423,7 @@
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, 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["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 6539436..8233b7b 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -12,7 +12,7 @@
class LoanSecurityShortfall(Document):
pass
-def update_shortfall_status(loan, security_value):
+def update_shortfall_status(loan, security_value, on_cancel=0):
loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall",
{"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1)
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index c4c2d68..b24dc2f 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -6,7 +6,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import get_datetime, flt
+from frappe.utils import get_datetime, flt, getdate
import json
from six import iteritems
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
@@ -113,7 +113,11 @@
pledged_qty += 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()
def get_pledged_security_qty(loan):
diff --git a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
index 2f4fe24..3d07081 100644
--- a/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
+++ b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
@@ -70,7 +70,9 @@
{
"fieldname": "loan_repayment_entry",
"fieldtype": "Link",
+ "hidden": 1,
"label": "Loan Repayment Entry",
+ "no_copy": 1,
"options": "Loan Repayment",
"read_only": 1
},
@@ -83,9 +85,10 @@
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-16 13:17:04.798335",
+ "modified": "2021-03-14 20:47:11.725818",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Salary Slip Loan",
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 4050a7d..7108338 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import unittest
import frappe
-from frappe.utils import cstr
+from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
@@ -81,15 +81,27 @@
bom = frappe.copy_doc(test_records[2])
bom.insert()
- # test amounts in selected currency
- self.assertEqual(bom.operating_cost, 100)
- self.assertEqual(bom.raw_material_cost, 351.68)
- self.assertEqual(bom.total_cost, 451.68)
+ raw_material_cost = 0.0
+ op_cost = 0.0
+
+ for op_row in bom.operations:
+ op_cost += op_row.operating_cost
+
+ for row in bom.items:
+ raw_material_cost += row.amount
+
+ base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
+ base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
+
+ # test amounts in selected currency, almostEqual checks for 7 digits by default
+ self.assertAlmostEqual(bom.operating_cost, op_cost)
+ self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost)
+ self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost)
# test amounts in selected currency
- self.assertEqual(bom.base_operating_cost, 6000)
- self.assertEqual(bom.base_raw_material_cost, 21100.80)
- self.assertEqual(bom.base_total_cost, 27100.80)
+ self.assertAlmostEqual(bom.base_operating_cost, base_op_cost)
+ self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
+ self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 8aa0ffd..92074c6 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -47,6 +47,8 @@
if d.completed_qty:
self.total_completed_qty += d.completed_qty
+ self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
+
def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index 9b1a8ca..032c9cd 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -11,10 +11,9 @@
},
display_sequence_id_column: function(frm) {
- frappe.meta.get_docfield("BOM Operation", "sequence_id",
- frm.doc.name).in_list_view = true;
-
- frm.fields_dict.operations.grid.refresh();
+ frm.fields_dict.operations.grid.update_docfield_property(
+ 'sequence_id', 'in_list_view', 1
+ );
},
calculate_operating_cost: function(frm, child) {
@@ -69,4 +68,4 @@
const d = locals[cdt][cdn];
frm.events.calculate_operating_cost(frm, d);
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 1686314..76d3c41 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -756,11 +756,17 @@
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
-erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
+erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.setup_uae_vat_fields
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
+erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v13_0.rename_discharge_date_in_ip_record
+erpnext.patches.v12_0.create_taxable_value_field
+erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status
+erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
new file mode 100644
index 0000000..3b560fd
--- /dev/null
+++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
@@ -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])
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py
new file mode 100644
index 0000000..4d649dd
--- /dev/null
+++ b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py
@@ -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)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py
new file mode 100644
index 0000000..387e885
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_status_field.py
@@ -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)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
new file mode 100644
index 0000000..bf8f566
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
@@ -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()
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
new file mode 100644
index 0000000..1208222
--- /dev/null
+++ b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
@@ -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)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py
new file mode 100644
index 0000000..a0c9fcf
--- /dev/null
+++ b/erpnext/patches/v12_0/create_taxable_value_field.py
@@ -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)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
similarity index 100%
rename from erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
rename to erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
diff --git a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py
index 5920bf1..a78f802 100644
--- a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py
+++ b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py
@@ -18,6 +18,7 @@
for old_dt, new_dt in doctypes.items():
if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt):
+ frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt))
frappe.rename_doc('DocType', old_dt, new_dt, force=True)
frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt))
frappe.delete_doc_if_exists('DocType', old_dt)
@@ -36,6 +37,18 @@
SET parentfield = %(parentfield)s
""".format(doctype), {'parentfield': parentfield})
+ # copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
+ frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""")
+ frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""")
+ frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""")
+ frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""")
+ frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
+ frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
+ frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""")
+ frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""")
+ frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""")
+ frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""")
+
# rename field
frappe.reload_doc('healthcare', 'doctype', 'lab_test')
if frappe.db.has_column('Lab Test', 'special_toggle'):
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
index d968e1f..021bb72 100644
--- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -20,9 +20,11 @@
frappe.clear_cache()
frappe.flags.warehouse_account_map = {}
+ company_list = []
+
data = frappe.db.sql('''
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
`tabStock Ledger Entry`
WHERE
@@ -36,6 +38,9 @@
total_sle = len(data)
i = 0
for d in data:
+ if d.company not in company_list:
+ company_list.append(d.company)
+
update_entries_after({
"item_code": d.item_code,
"warehouse": d.warehouse,
@@ -53,8 +58,10 @@
print("Reposting General Ledger Entries...")
- for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
- update_gl_entries_after(posting_date, posting_time, company=row.name)
+ if data:
+ 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
diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
index de08aa2..5316c01 100644
--- a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
+++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
@@ -6,6 +6,9 @@
if "Healthcare" not in frappe.get_active_domains():
return
+ frappe.reload_doc("healthcare", "doctype", "Therapy Session")
+ frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
+ frappe.reload_doc("healthcare", "doctype", "Clinical Procedure")
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
diff --git a/erpnext/patches/v7_1/update_lead_source.py b/erpnext/patches/v7_1/update_lead_source.py
index 517e66c..a2a48a6 100644
--- a/erpnext/patches/v7_1/update_lead_source.py
+++ b/erpnext/patches/v7_1/update_lead_source.py
@@ -5,7 +5,7 @@
def execute():
from erpnext.setup.setup_wizard.operations.install_fixtures import default_lead_sources
- frappe.reload_doc('selling', 'doctype', 'lead_source')
+ frappe.reload_doc('crm', 'doctype', 'lead_source')
frappe.local.lang = frappe.db.get_default("lang") or 'en'
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 395e56f..85bb651 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -133,45 +133,59 @@
}
};
});
+
+ frm.set_query('employee', 'employees', () => {
+ if (!frm.doc.company) {
+ frappe.msgprint(__("Please set a Company"));
+ return [];
+ }
+ return {
+ query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
+ filters: frm.events.get_employee_filters(frm)
+ };
+ });
+ },
+
+ get_employee_filters: function (frm) {
+ let filters = {};
+ filters['company'] = frm.doc.company;
+ filters['start_date'] = frm.doc.start_date;
+ filters['end_date'] = frm.doc.end_date;
+
+ if (frm.doc.department) {
+ filters['department'] = frm.doc.department;
+ }
+ if (frm.doc.branch) {
+ filters['branch'] = frm.doc.branch;
+ }
+ if (frm.doc.designation) {
+ filters['designation'] = frm.doc.designation;
+ }
+ if (frm.doc.employees) {
+ filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
+ }
+ return filters;
},
payroll_frequency: function (frm) {
frm.trigger("set_start_end_dates").then( ()=> {
frm.events.clear_employee_table(frm);
- frm.events.get_employee_with_salary_slip_and_set_query(frm);
- });
- },
-
- employee_filters: function (frm, emp_list) {
- frm.set_query('employee', 'employees', () => {
- return {
- filters: {
- name: ["not in", emp_list]
- }
- };
- });
- },
-
- get_employee_with_salary_slip_and_set_query: function (frm) {
- frappe.db.get_list('Salary Slip', {
- filters: {
- start_date: frm.doc.start_date,
- end_date: frm.doc.end_date,
- docstatus: 1,
- },
- fields: ['employee']
- }).then((emp) => {
- var emp_list = [];
- emp.forEach((employee_data) => {
- emp_list.push(Object.values(employee_data)[0]);
- });
- frm.events.employee_filters(frm, emp_list);
});
},
company: function (frm) {
frm.events.clear_employee_table(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ frm.trigger("set_payable_account_and_currency");
+ },
+
+ set_payable_account_and_currency: function (frm) {
+ frappe.db.get_value("Company", {"name": frm.doc.company}, "default_currency", (r) => {
+ frm.set_value('currency', r.default_currency);
+ });
+ frappe.db.get_value("Company", {"name": frm.doc.company}, "default_payroll_payable_account", (r) => {
+ frm.set_value('payroll_payable_account', r.default_payroll_payable_account);
+ });
},
currency: function (frm) {
@@ -345,11 +359,3 @@
})
);
};
-
-frappe.ui.form.on('Payroll Employee Detail', {
- employee: function(frm) {
- if (!frm.doc.payroll_frequency) {
- frappe.throw(__("Please set a Payroll Frequency"));
- }
- }
-});
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 7890471..fde2e07 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -10,16 +10,17 @@
from frappe import _
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from frappe.desk.reportview import get_match_cond, get_filters_cond
class PayrollEntry(Document):
def onload(self):
if not self.docstatus==1 or self.salary_slips_submitted:
- return
+ return
# check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees):
- self.set_onload("submitted_ss", True)
+ self.set_onload("submitted_ss", True)
def validate(self):
self.number_of_employees = len(self.employees)
@@ -59,16 +60,16 @@
condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency}
sal_struct = frappe.db.sql_list("""
- select
- name from `tabSalary Structure`
- where
- docstatus = 1 and
- is_active = 'Yes'
- and company = %(company)s
- and currency = %(currency)s and
- ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
- {condition}""".format(condition=condition),
- {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
+ select
+ name from `tabSalary Structure`
+ where
+ docstatus = 1 and
+ is_active = 'Yes'
+ and company = %(company)s
+ and currency = %(currency)s and
+ ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
+ {condition}""".format(condition=condition),
+ {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
if sal_struct:
cond += "and t2.salary_structure IN %(sal_struct)s "
@@ -176,15 +177,15 @@
"""
Returns list of salary slips based on selected criteria
"""
- cond = self.get_filter_condition()
ss_list = frappe.db.sql("""
select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1
- where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s
- and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s %s
- """ % ('%s', '%s', '%s','%s', cond), (ss_status, self.start_date, self.end_date, self.salary_slip_based_on_timesheet), as_dict=as_dict)
+ where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s
+ and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s
+ """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict)
return ss_list
+ @frappe.whitelist()
def submit_salary_slips(self):
self.check_permission('write')
ss_list = self.get_sal_slip_list(ss_status=0)
@@ -270,26 +271,26 @@
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount += flt(amount, precision)
accounts.append({
- "account": acc_cc[0],
- "debit_in_account_currency": flt(amt, precision),
- "exchange_rate": flt(exchange_rate),
- "party_type": '',
- "cost_center": acc_cc[1] or self.cost_center,
- "project": self.project
- })
+ "account": acc_cc[0],
+ "debit_in_account_currency": flt(amt, precision),
+ "exchange_rate": flt(exchange_rate),
+ "party_type": '',
+ "cost_center": acc_cc[1] or self.cost_center,
+ "project": self.project
+ })
# Deductions
for acc_cc, amount in deductions.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount -= flt(amount, precision)
accounts.append({
- "account": acc_cc[0],
- "credit_in_account_currency": flt(amt, precision),
- "exchange_rate": flt(exchange_rate),
- "cost_center": acc_cc[1] or self.cost_center,
- "party_type": '',
- "project": self.project
- })
+ "account": acc_cc[0],
+ "credit_in_account_currency": flt(amt, precision),
+ "exchange_rate": flt(exchange_rate),
+ "cost_center": acc_cc[1] or self.cost_center,
+ "party_type": '',
+ "project": self.project
+ })
# Payable amount
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
@@ -335,10 +336,9 @@
def make_payment_entry(self):
self.check_permission('write')
- cond = self.get_filter_condition()
salary_slip_name_list = frappe.db.sql(""" select t1.name from `tabSalary Slip` t1
- where t1.docstatus = 1 and start_date >= %s and end_date <= %s %s
- """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_list = True)
+ where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s
+ """, (self.start_date, self.end_date, self.name), as_list = True)
if salary_slip_name_list and len(salary_slip_name_list) > 0:
salary_slip_total = 0
@@ -370,20 +370,20 @@
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
accounts.append({
- "account": self.payment_account,
- "bank_account": self.bank_account,
- "credit_in_account_currency": flt(amount, precision),
- "exchange_rate": flt(exchange_rate),
- })
+ "account": self.payment_account,
+ "bank_account": self.bank_account,
+ "credit_in_account_currency": flt(amount, precision),
+ "exchange_rate": flt(exchange_rate),
+ })
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
accounts.append({
- "account": payroll_payable_account,
- "debit_in_account_currency": flt(amount, precision),
- "exchange_rate": flt(exchange_rate),
- "reference_type": self.doctype,
- "reference_name": self.name
- })
+ "account": payroll_payable_account,
+ "debit_in_account_currency": flt(amount, precision),
+ "exchange_rate": flt(exchange_rate),
+ "reference_type": self.doctype,
+ "reference_name": self.name
+ })
if len(currencies) > 1:
multi_currency = 1
@@ -409,6 +409,7 @@
self.update(get_start_end_dates(self.payroll_frequency,
self.start_date or self.posting_date, self.company))
+ @frappe.whitelist()
def validate_employee_attendance(self):
employees_to_mark_attendance = []
days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0
@@ -424,7 +425,7 @@
employees_to_mark_attendance.append({
"employee": employee_detail.employee,
"employee_name": employee_detail.employee_name
- })
+ })
return employees_to_mark_attendance
def get_count_holidays_of_employee(self, employee, start_date):
@@ -441,11 +442,11 @@
def get_count_employee_attendance(self, employee, start_date):
marked_days = 0
attendances = frappe.get_all("Attendance",
- fields = ["count(*)"],
- filters = {
- "employee": employee,
- "attendance_date": ('between', [start_date, self.end_date])
- }, as_list=1)
+ fields = ["count(*)"],
+ filters = {
+ "employee": employee,
+ "attendance_date": ('between', [start_date, self.end_date])
+ }, as_list=1)
if attendances and attendances[0][0]:
marked_days = attendances[0][0]
return marked_days
@@ -553,6 +554,7 @@
def create_salary_slips_for_employees(employees, args, publish_progress=True):
salary_slips_exists_for = get_existing_salary_slips(employees, args)
count=0
+ salary_slips_not_created = []
for emp in employees:
if emp not in salary_slips_exists_for:
args.update({
@@ -566,33 +568,24 @@
frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)),
title = _("Creating Salary Slips..."))
else:
- salary_slip_name = frappe.db.sql(
- '''SELECT
- name
- FROM `tabSalary Slip`
- WHERE company=%s
- AND start_date >= %s
- AND end_date <= %s
- AND employee = %s
- ''', (args.company, args.start_date, args.end_date, emp), as_dict=True)
-
- salary_slip_doc = frappe.get_doc('Salary Slip', salary_slip_name[0].name)
- salary_slip_doc.exchange_rate = args.exchange_rate
- salary_slip_doc.set_totals()
- salary_slip_doc.db_update()
+ salary_slips_not_created.append(emp)
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
payroll_entry.db_set("salary_slips_created", 1)
payroll_entry.notify_update()
+ if salary_slips_not_created:
+ frappe.msgprint(_("Salary Slips already exists for employees {}, and will not be processed by this payroll.")
+ .format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))) , title=_("Message"), indicator="orange")
+
def get_existing_salary_slips(employees, args):
return frappe.db.sql_list("""
select distinct employee from `tabSalary Slip`
- where docstatus!= 2 and company = %s
+ where docstatus!= 2 and company = %s and payroll_entry = %s
and start_date >= %s and end_date <= %s
and employee in (%s)
- """ % ('%s', '%s', '%s', ', '.join(['%s']*len(employees))),
- [args.company, args.start_date, args.end_date] + employees)
+ """ % ('%s', '%s', '%s', '%s', ', '.join(['%s']*len(employees))),
+ [args.company, args.payroll_entry, args.start_date, args.end_date] + employees)
def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
submitted_ss = []
@@ -644,3 +637,61 @@
'txt': "%%%s%%" % frappe.db.escape(txt),
'start': start, 'page_len': page_len
})
+
+def get_employee_with_existing_salary_slip(start_date, end_date, company):
+ return frappe.db.sql_list("""
+ select employee from `tabSalary Slip`
+ where
+ (start_date between %(start_date)s and %(end_date)s
+ or
+ end_date between %(start_date)s and %(end_date)s
+ or
+ %(start_date)s between start_date and end_date)
+ and company = %(company)s
+ and docstatus = 1
+ """, {'start_date': start_date, 'end_date': end_date, 'company': company})
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def employee_query(doctype, txt, searchfield, start, page_len, filters):
+ filters = frappe._dict(filters)
+ conditions = []
+ exclude_employees = []
+ emp_cond = ''
+ if filters.start_date and filters.end_date:
+ employee_list = get_employee_with_existing_salary_slip(filters.start_date, filters.end_date, filters.company)
+ emp = filters.get('employees')
+ filters.pop('start_date')
+ filters.pop('end_date')
+ if filters.employees is not None:
+ filters.pop('employees')
+ if employee_list:
+ exclude_employees.extend(employee_list)
+ if emp:
+ exclude_employees.extend(emp)
+ if exclude_employees:
+ emp_cond += 'and employee not in %(exclude_employees)s'
+
+ return frappe.db.sql("""select name, employee_name from `tabEmployee`
+ where status = 'Active'
+ and docstatus < 2
+ and ({key} like %(txt)s
+ or employee_name like %(txt)s)
+ {emp_cond}
+ {fcond} {mcond}
+ order by
+ if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
+ if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
+ idx desc,
+ name, employee_name
+ limit %(start)s, %(page_len)s""".format(**{
+ 'key': searchfield,
+ 'fcond': get_filters_cond(doctype, filters, conditions),
+ 'mcond': get_match_cond(doctype),
+ 'emp_cond': emp_cond
+ }), {
+ 'txt': "%%%s%%" % txt,
+ '_txt': txt.replace("%", ""),
+ 'start': start,
+ 'page_len': page_len,
+ 'exclude_employees': exclude_employees})
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index 84c3814..7528bf7 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -51,21 +51,22 @@
company_doc = frappe.get_doc('Company', company)
salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD')
- create_salary_structure_assignment(employee, salary_structure.name, company=company)
+ create_salary_structure_assignment(employee, salary_structure.name, company=company, currency='USD')
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})))
salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure")
dates = get_start_end_dates('Monthly', nowdate())
- payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date,
+ payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date,
payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70)
payroll_entry.make_payment_entry()
salary_slip.load_from_db()
payroll_je = salary_slip.journal_entry
- payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je)
+ if payroll_je:
+ payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je)
- self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
- self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
+ self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
+ self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
payment_entry = frappe.db.sql('''
Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index 3e8a213..e3993fa 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -39,7 +39,8 @@
frm.set_query("employee", function() {
return {
- query: "erpnext.controllers.queries.employee_query"
+ query: "erpnext.controllers.queries.employee_query",
+ filters: frm.doc.company
};
});
},
@@ -93,28 +94,31 @@
},
set_exchange_rate: function(frm, company_currency) {
- if (frm.doc.currency) {
- var from_currency = frm.doc.currency;
- if (from_currency != company_currency) {
- frm.events.hide_loan_section(frm);
- frappe.call({
- method: "erpnext.setup.utils.get_exchange_rate",
- args: {
- from_currency: from_currency,
- to_currency: company_currency,
- },
- callback: function(r) {
- frm.set_value("exchange_rate", flt(r.message));
- frm.set_df_property("exchange_rate", "hidden", 0);
- frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
- + " = [?] " + company_currency);
- }
- });
- } else {
- frm.set_value("exchange_rate", 1.0);
- frm.set_df_property("exchange_rate", "hidden", 1);
- frm.set_df_property("exchange_rate", "description", "");
- }
+ if (frm.doc.docstatus === 0) {
+ if (frm.doc.currency) {
+ var from_currency = frm.doc.currency;
+ if (from_currency != company_currency) {
+ frm.events.hide_loan_section(frm);
+ frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ from_currency: from_currency,
+ to_currency: company_currency,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value("exchange_rate", flt(r.message));
+ frm.set_df_property('exchange_rate', 'hidden', 0);
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ + " = [?] " + company_currency);
+ }
+ }
+ });
+ } else {
+ frm.set_value("exchange_rate", 1.0);
+ frm.set_df_property('exchange_rate', 'hidden', 1);
+ frm.set_df_property("exchange_rate", "description", "" );
+ }
}
},
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index aa9acd8..f6d4c7b 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -124,9 +124,12 @@
def check_existing(self):
if not self.salary_slip_based_on_timesheet:
+ cond = ""
+ if self.payroll_entry:
+ cond += "and payroll_entry = '{0}'".format(self.payroll_entry)
ret_exist = frappe.db.sql("""select name from `tabSalary Slip`
where start_date = %s and end_date = %s and docstatus != 2
- and employee = %s and name != %s""",
+ and employee = %s and name != %s {0}""".format(cond),
(self.start_date, self.end_date, self.employee, self.name))
if ret_exist:
self.employee = ''
@@ -1053,7 +1056,7 @@
repayment_entry.save()
repayment_entry.submit()
- loan.loan_repayment_entry = repayment_entry.name
+ frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
def cancel_loan_repayment_entry(self):
for loan in self.loans:
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 6aa1387..b539b1b 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -111,12 +111,19 @@
frappe.set_route('Form', 'Salary Structure Assignment', doc.name);
});
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"];
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');
},
diff --git a/erpnext/portal/doctype/products_settings/products_settings.js b/erpnext/portal/doctype/products_settings/products_settings.js
index b68b5d7..2f8b037 100644
--- a/erpnext/portal/doctype/products_settings/products_settings.js
+++ b/erpnext/portal/doctype/products_settings/products_settings.js
@@ -10,10 +10,12 @@
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
- const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
- field.fieldtype = 'Select';
- field.options = valid_fields;
- frm.fields_dict.filter_fields.grid.refresh();
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'fieldtype', 'Select'
+ );
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'options', valid_fields
+ );
});
}
});
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index a92bad1..70139c6 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -33,12 +33,16 @@
def test_project_template_having_parent_child_tasks(self):
project_name = "Test Project with Template - Tasks with Parent-Child Relation"
+
+ if frappe.db.get_value('Project', {'project_name': project_name}, 'name'):
+ project_name = frappe.db.get_value('Project', {'project_name': project_name}, 'name')
+
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
frappe.delete_doc('Project', project_name)
task1 = task_exists("Test Template Task Parent")
if not task1:
- task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4)
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10)
task2 = task_exists("Test Template Task Child 1")
if not task2:
@@ -53,7 +57,7 @@
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4))
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 10))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index 649eb45..ceeecb2 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -276,74 +276,3 @@
}
}
}
-
-
-// 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;
-}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 21a20a7..6c2144d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -737,28 +737,34 @@
this.frm.trigger("item_code", cdt, cdn);
}
else {
- var valid_serial_nos = [];
- var serialnos = [];
// Replacing all occurences of comma with carriage return
item.serial_no = item.serial_no.replace(/,/g, '\n');
- serialnos = item.serial_no.split("\n");
- for (var i = 0; i < serialnos.length; i++) {
- if (serialnos[i] != "") {
- valid_serial_nos.push(serialnos[i]);
- }
- }
item.conversion_factor = item.conversion_factor || 1;
-
refresh_field("serial_no", item.name, item.parentfield);
- if(!doc.is_return && cint(user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
- frappe.model.set_value(item.doctype, item.name,
- "qty", valid_serial_nos.length / item.conversion_factor);
- frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
+ if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
+ setTimeout(() => {
+ me.update_qty(cdt, cdn);
+ }, 10000);
}
}
}
},
+ update_qty: function(cdt, cdn) {
+ var valid_serial_nos = [];
+ var serialnos = [];
+ var item = frappe.get_doc(cdt, cdn);
+ serialnos = item.serial_no.split("\n");
+ for (var i = 0; i < serialnos.length; i++) {
+ if (serialnos[i] != "") {
+ valid_serial_nos.push(serialnos[i]);
+ }
+ }
+ frappe.model.set_value(item.doctype, item.name,
+ "qty", valid_serial_nos.length / item.conversion_factor);
+ frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
+ },
+
validate: function() {
this.calculate_taxes_and_totals(false);
},
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
index db8bda7..68ed339 100644
--- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -8,6 +8,7 @@
"enable",
"section_break_2",
"sandbox_mode",
+ "applicable_from",
"credentials",
"auth_token",
"token_expiry"
@@ -48,12 +49,19 @@
"fieldname": "sandbox_mode",
"fieldtype": "Check",
"label": "Sandbox Mode"
+ },
+ {
+ "fieldname": "applicable_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Applicable From",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-01-13 12:04:49.449199",
+ "modified": "2021-03-30 12:26:25.538294",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
index dd9d997..a65b1ca 100644
--- a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
@@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "company",
"gstin",
"username",
"password"
@@ -30,12 +31,20 @@
"in_list_view": 1,
"label": "Password",
"reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-22 15:10:53.466205",
+ "modified": "2021-03-22 12:16:56.365616",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 7cd64f2..c1a222a 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,12 +1,13 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
- const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
- const supply_type = frm.doc.gst_category;
- const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
- const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
+ const res = await frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
+ args: { doc: frm.doc }
+ });
+ const invoice_eligible = res.message;
- if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
+ if (!invoice_eligible) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@@ -109,45 +110,25 @@
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
- const fields = [
- {
- "label": "Reason",
- "fieldname": "reason",
- "fieldtype": "Select",
- "reqd": 1,
- "default": "1-Duplicate",
- "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
- },
- {
- "label": "Remark",
- "fieldname": "remark",
- "fieldtype": "Data",
- "reqd": 1
- }
- ];
const action = () => {
- const d = new frappe.ui.Dialog({
- title: __('Cancel E-Way Bill'),
- fields: fields,
+ let message = __('Cancellation of e-way bill is currently not supported. ');
+ message += '<br><br>';
+ message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
+
+ frappe.msgprint({
+ title: __('Update E-Way Bill Cancelled Status?'),
+ message: message,
+ indicator: 'orange',
primary_action: function() {
- const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
- args: {
- doctype,
- docname: name,
- eway_bill: ewaybill,
- reason: data.reason.split('-')[0],
- remark: data.remark
- },
+ args: { doctype, docname: name },
freeze: true,
- callback: () => frm.reload_doc() || d.hide(),
- error: () => d.hide()
+ callback: () => frm.reload_doc()
});
},
- primary_action_label: __('Submit')
+ primary_action_label: __('Yes')
});
- d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 8eccc3f..fb1f464 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -15,18 +15,43 @@
import io
from frappe import _, bold
from pyqrcode import create as qrcreate
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.scheduler import is_scheduler_inactive
+from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
-from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
+from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form, getdate, time_diff_in_hours
-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'
+@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
+
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes')
- if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
+ if invalid_supply_type or company_transaction or no_taxes_applied:
+ return False
+
+ return True
+
+def validate_einvoice_fields(doc):
+ invoice_eligible = validate_eligibility(doc)
+
+ if not invoice_eligible:
return
if doc.docstatus == 0 and doc._action == 'save':
@@ -35,6 +60,8 @@
if len(doc.name) > 16:
raise_document_name_too_long_error()
+ doc.einvoice_status = 'Pending'
+
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
@@ -76,6 +103,9 @@
))
def get_doc_details(invoice):
+ if getdate(invoice.posting_date) < getdate('2021-01-01'):
+ frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed'))
+
invoice_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name
@@ -87,56 +117,39 @@
invoice_date=invoice_date
))
-def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
- d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
-
- if ((not d.gstin and not shipping_address)
- or not d.city
- or not d.pincode
- or not d.address_title
- or not d.address_line1
- or not d.gst_state_number):
+def validate_address_fields(address, is_shipping_address):
+ if ((not address.gstin and not is_shipping_address)
+ or not address.city
+ or not address.pincode
+ or not address.address_title
+ or not address.address_line1
+ or not address.gst_state_number):
frappe.throw(
- msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
- get_link_to_form('Address', address_name)
- ),
+ msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
title=_('Missing Address Fields')
)
- if d.gst_state_number == 97:
+def get_party_details(address_name, is_shipping_address=False):
+ addr = frappe.get_doc('Address', address_name)
+
+ validate_address_fields(addr, is_shipping_address)
+
+ if addr.gst_state_number == 97:
# according to einvoice standard
- pincode = 999999
+ addr.pincode = 999999
party_address_details = frappe._dict(dict(
- legal_name=sanitize_for_json(d.address_title),
- location=sanitize_for_json(d.city),
- pincode=d.pincode,
- state_code=d.gst_state_number,
- address_line1=sanitize_for_json(d.address_line1),
- address_line2=sanitize_for_json(d.address_line2)
+ legal_name=sanitize_for_json(addr.address_title),
+ location=sanitize_for_json(addr.city),
+ pincode=addr.pincode, gstin=addr.gstin,
+ state_code=addr.gst_state_number,
+ address_line1=sanitize_for_json(addr.address_line1),
+ address_line2=sanitize_for_json(addr.address_line2)
))
- if d.gstin:
- party_address_details.gstin = d.gstin
+
return party_address_details
-def get_gstin_details(gstin):
- if not hasattr(frappe.local, 'gstin_cache'):
- frappe.local.gstin_cache = {}
-
- key = gstin
- details = frappe.local.gstin_cache.get(key)
- if details:
- return details
-
- details = frappe.cache().hget('gstin_cache', key)
- if details:
- frappe.local.gstin_cache[key] = details
- return details
-
- if not details:
- return GSPConnector.get_gstin_details(gstin)
-
def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
@@ -171,10 +184,15 @@
item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
- item.discount_amount = 0
- item.unit_rate = abs(item.base_net_amount / item.qty)
- item.gross_amount = abs(item.base_net_amount)
- item.taxable_value = abs(item.base_net_amount)
+
+ if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
+ item.discount_amount = abs(item.base_amount - item.base_net_amount)
+ else:
+ item.discount_amount = 0
+
+ item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
+ item.gross_amount = abs(item.taxable_value) + item.discount_amount
+ item.taxable_value = abs(item.taxable_value)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
@@ -207,11 +225,11 @@
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
if is_applicable:
# this contains item wise tax rate & tax amount (incl. discount)
- item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
- item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
+ item_tax_amount = (item_tax_rate / 100) * item.taxable_value
if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1]
@@ -225,6 +243,9 @@
if t.account_head in gst_accounts[f'{tax_type}_account']:
item.tax_rate += item_tax_rate
item[f'{tax_type}_amount'] += abs(item_tax_amount)
+ else:
+ # TODO: other charges per item
+ pass
return item
@@ -232,10 +253,14 @@
invoice_value_details = frappe._dict(dict())
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
- invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
+ # Discount already applied on net total which means on items
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
+ invoice_value_details.invoice_discount_amt = 0
+ elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount:
+ invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
else:
- invoice_value_details.base_total = abs(invoice.base_net_total)
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
# since tax already considers discount amount
invoice_value_details.invoice_discount_amt = 0
@@ -256,7 +281,11 @@
invoice_value_details.total_igst_amt = 0
invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0
+ considered_rows = []
+
for t in invoice.taxes:
+ tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \
+ else t.base_tax_amount_after_discount_amount
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
# using after discount amt since item also uses after discount amt for cess calc
@@ -264,12 +293,26 @@
for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']:
- invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
+
+ invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
+ update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
else:
- invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
+ invoice_value_details.total_other_charges += abs(tax_amount)
return invoice_value_details
+def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
+ prev_row_id = cint(tax_row.row_id) - 1
+ if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
+ if tax_row.charge_type == 'On Previous Row Amount':
+ amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+ if tax_row.charge_type == 'On Previous Row Total':
+ amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
@@ -282,6 +325,10 @@
))
def get_return_doc_reference(invoice):
+ if not invoice.return_against:
+ frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
+ .format(frappe.bold('Return Against')), title=_('Missing Field'))
+
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
@@ -289,7 +336,11 @@
def get_eway_bill_details(invoice):
if invoice.is_return:
- frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
+ frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
+ title=_('Invalid Fields'))
+
+ if not invoice.distance:
+ frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@@ -307,9 +358,15 @@
def validate_mandatory_fields(invoice):
if not invoice.company_address:
- frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
+ frappe.throw(
+ _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
if not invoice.customer_address:
- frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
+ frappe.throw(
+ _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
@@ -321,6 +378,39 @@
title=_('Missing Fields')
)
+def validate_totals(einvoice):
+ item_list = einvoice['ItemList']
+ value_details = einvoice['ValDtls']
+
+ total_item_ass_value = 0
+ total_item_cgst_value = 0
+ total_item_sgst_value = 0
+ total_item_igst_value = 0
+ total_item_value = 0
+ for item in item_list:
+ total_item_ass_value += flt(item['AssAmt'])
+ total_item_cgst_value += flt(item['CgstAmt'])
+ total_item_sgst_value += flt(item['SgstAmt'])
+ total_item_igst_value += flt(item['IgstAmt'])
+ total_item_value += flt(item['TotItemVal'])
+
+ if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
+ frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
+
+ if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
+ frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
+
+ if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - total_item_value) > 1:
+ frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
+
+ calculated_invoice_value = \
+ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ + flt(value_details['OthChrg']) - flt(value_details['Discount'])
+
+ if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
+ frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
+
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
@@ -330,12 +420,12 @@
item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice)
invoice_value_details = get_invoice_value_details(invoice)
- seller_details = get_party_details(invoice.company_address, company_address=1)
+ seller_details = get_party_details(invoice.company_address)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
- buyer_details = get_party_details(invoice.customer_address, billing_address=1)
+ buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
if place_of_supply:
place_of_supply = place_of_supply.split('-')[0]
@@ -343,20 +433,23 @@
place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
+ seller_details.update(dict(legal_name=invoice.company))
+ buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
+
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
- shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
+ shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
- if invoice.is_return and invoice.return_against:
+ if invoice.is_return:
prev_doc_details = get_return_doc_reference(invoice)
- if invoice.transporter:
+ if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
@@ -369,18 +462,70 @@
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
- einvoice = safe_json_load(einvoice)
- validations = json.loads(read_json('einv_validation'))
- errors = validate_einvoice(validations, einvoice)
- if errors:
- message = "\n".join([
- "E Invoice: ", json.dumps(einvoice, indent=4),
- "-" * 50,
- "Errors: ", json.dumps(errors, indent=4)
- ])
- frappe.log_error(title="E Invoice Validation Failed", message=message)
- frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
+ try:
+ einvoice = safe_json_load(einvoice)
+ einvoice = santize_einvoice_fields(einvoice)
+ validate_totals(einvoice)
+
+ except Exception:
+ log_error(einvoice)
+ link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
+ frappe.throw(
+ _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
+ invoice.name, link_to_error_list),
+ title=_('E Invoice Creation Failed')
+ )
+
+ return einvoice
+
+def log_error(data=None):
+ if not isinstance(data, dict):
+ data = json.loads(data)
+
+ seperator = "--" * 50
+ err_tb = traceback.format_exc()
+ err_msg = str(sys.exc_info()[1])
+ data = json.dumps(data, indent=4)
+
+ message = "\n".join([
+ "Error", err_msg, seperator,
+ "Data:", data, seperator,
+ "Exception:", err_tb
+ ])
+ frappe.log_error(title=_('E Invoice Request Failed'), message=message)
+
+def santize_einvoice_fields(einvoice):
+ int_fields = ["Pin","Distance","CrDay"]
+ float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
+ copy = einvoice.copy()
+ for key, value in copy.items():
+ if isinstance(value, list):
+ for idx, d in enumerate(value):
+ santized_dict = santize_einvoice_fields(d)
+ if santized_dict:
+ einvoice[key][idx] = santized_dict
+ else:
+ einvoice[key].pop(idx)
+
+ if not einvoice[key]:
+ einvoice.pop(key, None)
+
+ elif isinstance(value, dict):
+ santized_dict = santize_einvoice_fields(value)
+ if santized_dict:
+ einvoice[key] = santized_dict
+ else:
+ einvoice.pop(key, None)
+
+ elif not value or value == "None":
+ einvoice.pop(key, None)
+
+ elif key in float_fields:
+ einvoice[key] = flt(value, 2)
+
+ elif key in int_fields:
+ einvoice[key] = cint(value)
return einvoice
@@ -396,72 +541,22 @@
snippet = json_string[start:end]
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
-def validate_einvoice(validations, einvoice, errors=None):
- if errors is None:
- errors = []
- for fieldname, field_validation in validations.items():
- value = einvoice.get(fieldname, None)
- if not value or value == "None":
- # remove keys with empty values
- einvoice.pop(fieldname, None)
- continue
-
- value_type = field_validation.get("type").lower()
- if value_type in ['object', 'array']:
- child_validations = field_validation.get('properties')
-
- if isinstance(value, list):
- for d in value:
- validate_einvoice(child_validations, d, errors)
- if not d:
- # remove empty dicts
- einvoice.pop(fieldname, None)
- else:
- validate_einvoice(child_validations, value, errors)
- if not value:
- # remove empty dicts
- einvoice.pop(fieldname, None)
- continue
-
- # convert to int or str
- if value_type == 'string':
- einvoice[fieldname] = str(value)
- elif value_type == 'number':
- is_integer = '.' not in str(field_validation.get('maximum'))
- precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
- einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
- value = einvoice[fieldname]
-
- max_length = field_validation.get('maxLength')
- minimum = flt(field_validation.get('minimum'))
- maximum = flt(field_validation.get('maximum'))
- pattern_str = field_validation.get('pattern')
- pattern = re.compile(pattern_str or '')
-
- label = field_validation.get('description') or fieldname
-
- if value_type == 'string' and len(value) > max_length:
- errors.append(_('{} should not exceed {} characters').format(label, max_length))
- if value_type == 'number' and (value > maximum or value < minimum):
- errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
- if pattern_str and not pattern.match(value):
- errors.append(field_validation.get('validationMsg'))
-
- return errors
-
-class RequestFailed(Exception): pass
+class RequestFailed(Exception):
+ pass
+class CancellationNotAllowed(Exception):
+ pass
class GSPConnector():
def __init__(self, doctype=None, docname=None):
- self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
- sandbox_mode = self.e_invoice_settings.sandbox_mode
+ self.doctype = doctype
+ self.docname = docname
- self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
- self.credentials = self.get_credentials()
+ self.set_invoice()
+ self.set_credentials()
# authenticate url is same for sandbox & live
self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
- self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
+ self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test'
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
@@ -470,15 +565,26 @@
self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
- def get_credentials(self):
+ def set_invoice(self):
+ self.invoice = None
+ if self.doctype and self.docname:
+ self.invoice = frappe.get_cached_doc(self.doctype, self.docname)
+
+ def set_credentials(self):
+ self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+
+ if not self.e_invoice_settings.enable:
+ frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
+
if self.invoice:
gstin = self.get_seller_gstin()
- if not self.e_invoice_settings.enable:
- frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
- credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
+ credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
+ if credentials_for_gstin:
+ self.credentials = credentials_for_gstin[0]
+ else:
+ frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
else:
- credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
- return credentials
+ self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
@@ -529,7 +635,7 @@
self.e_invoice_settings.reload()
except Exception:
- self.log_error(res)
+ log_error(res)
self.raise_error(True)
def get_headers(self):
@@ -551,14 +657,14 @@
if res.get('success'):
return res.get('result')
else:
- self.log_error(res)
+ log_error(res)
raise RequestFailed
except RequestFailed:
self.raise_error()
except Exception:
- self.log_error()
+ log_error()
self.raise_error(True)
@staticmethod
@@ -576,12 +682,13 @@
return details
def generate_irn(self):
- headers = self.get_headers()
- einvoice = make_einvoice(self.invoice)
- data = json.dumps(einvoice, indent=4)
-
+ data = {}
try:
+ headers = self.get_headers()
+ einvoice = make_einvoice(self.invoice)
+ data = json.dumps(einvoice, indent=4)
res = self.make_request('post', self.generate_irn_url, headers, data)
+
if res.get('success'):
self.set_einvoice_data(res.get('result'))
@@ -601,12 +708,36 @@
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
self.raise_error(errors=errors)
- except Exception:
- self.log_error(data)
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
self.raise_error(True)
+ @staticmethod
+ def bulk_generate_irn(invoices):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ gsp_connector.generate_irn()
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
def get_irn_details(self, irn):
headers = self.get_headers()
@@ -623,21 +754,30 @@
self.raise_error(errors=errors)
except Exception:
- self.log_error()
+ log_error()
self.raise_error(True)
def cancel_irn(self, irn, reason, remark):
- headers = self.get_headers()
- data = json.dumps({
- 'Irn': irn,
- 'Cnlrsn': reason,
- 'Cnlrem': remark
- }, indent=4)
-
+ data, res = {}, {}
try:
+ # validate cancellation
+ if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24:
+ frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+ if not irn:
+ frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+
+ headers = self.get_headers()
+ data = json.dumps({
+ 'Irn': irn,
+ 'Cnlrsn': reason,
+ 'Cnlrem': remark
+ }, indent=4)
+
res = self.make_request('post', self.cancel_irn_url, headers, data)
- if res.get('success'):
+ if res.get('success') or '9999' in res.get('message'):
self.invoice.irn_cancelled = 1
+ self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else ""
+ self.invoice.einvoice_status = 'Cancelled'
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
@@ -650,12 +790,41 @@
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
self.raise_error(errors=errors)
- except Exception:
- self.log_error(data)
+ except CancellationNotAllowed as e:
+ self.set_failed_status(errors=str(e))
+ self.raise_error(errors=str(e))
+
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
self.raise_error(True)
+ @staticmethod
+ def bulk_cancel_irn(invoices, reason, remark):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ irn = gsp_connector.invoice.irn
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs)
@@ -694,7 +863,7 @@
self.raise_error(errors=errors)
except Exception:
- self.log_error(data)
+ log_error(data)
self.raise_error(True)
def cancel_eway_bill(self, eway_bill, reason, remark):
@@ -726,7 +895,7 @@
self.raise_error(errors=errors)
except Exception:
- self.log_error(data)
+ log_error(data)
self.raise_error(True)
def sanitize_error_message(self, message):
@@ -741,6 +910,9 @@
]
then we trim down the message by looping over errors
'''
+ if not message:
+ return []
+
errors = re.findall(': [^:]+', message)
for idx, e in enumerate(errors):
# remove colons
@@ -752,22 +924,6 @@
return errors
- def log_error(self, data={}):
- if not isinstance(data, dict):
- data = json.loads(data)
-
- seperator = "--" * 50
- err_tb = traceback.format_exc()
- err_msg = str(sys.exc_info()[1])
- data = json.dumps(data, indent=4)
-
- message = "\n".join([
- "Error", err_msg, seperator,
- "Data:", data, seperator,
- "Exception:", err_tb
- ])
- frappe.log_error(title=_('E Invoice Request Failed'), message=message)
-
def raise_error(self, raise_exception=False, errors=[]):
title = _('E Invoice Request Failed')
if errors:
@@ -787,8 +943,13 @@
self.invoice.irn = res.get('Irn')
self.invoice.ewaybill = res.get('EwbNo')
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
self.invoice.signed_einvoice = dec_signed_invoice
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
self.invoice.signed_qr_code = res.get('SignedQRCode')
+ self.invoice.einvoice_status = 'Generated'
self.attach_qrcode_image()
@@ -825,6 +986,17 @@
self.invoice.flags.ignore_validate = True
self.invoice.save()
+ def set_failed_status(self, errors=None):
+ frappe.db.rollback()
+ self.invoice.einvoice_status = 'Failed'
+ self.invoice.failure_description = self.get_failure_message(errors) if errors else ""
+ self.update_invoice()
+ frappe.db.commit()
+
+ def get_failure_message(self, errors):
+ if isinstance(errors, list):
+ errors = ', '.join(errors)
+ return errors
def sanitize_for_json(string):
"""Escape JSON specific characters from a string."""
@@ -854,5 +1026,114 @@
@frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
- gsp_connector = GSPConnector(doctype, docname)
- gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+ # TODO: uncomment when eway_bill api from Adequare is enabled
+ # gsp_connector = GSPConnector(doctype, docname)
+ # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+
+ # update cancelled status only, to be able to cancel irn next
+ frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
+
+@frappe.whitelist()
+def generate_einvoices(docnames):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices generated successfully').format(success),
+ title=_('Bulk E-Invoice Generation Complete')
+ )
+
+ else:
+ enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames)
+
+def schedule_bulk_generate_irn(docnames):
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_generation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def show_bulk_action_failure_message(failures):
+ for doc in failures:
+ docname = '<a href="sales-invoice/{0}">{0}</a>'.format(doc.get('docname'))
+ message = doc.get('message').replace("'", '"')
+ if message[0] == '[':
+ errors = json.loads(message)
+ error_list = ''.join(['<li>{}</li>'.format(err) for err in errors])
+ message = '''{} has following errors:<br>
+ <ul style="padding-left: 20px; padding-top: 5px">{}</ul>'''.format(docname, error_list)
+ else:
+ message = '{} - {}'.format(docname, message)
+
+ frappe.msgprint(
+ message,
+ title=_('Bulk E-Invoice Generation Complete'),
+ indicator='red'
+ )
+
+@frappe.whitelist()
+def cancel_irns(docnames, reason, remark):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices cancelled successfully').format(success),
+ title=_('Bulk E-Invoice Cancellation Complete')
+ )
+ else:
+ enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark)
+
+def schedule_bulk_cancel_irn(docnames, reason, remark):
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_cancellation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def enqueue_bulk_action(job, **kwargs):
+ check_scheduler_status()
+
+ enqueue(
+ job,
+ **kwargs,
+ queue="long",
+ timeout=10000,
+ event="processing_bulk_einvoice_action",
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+
+ if job == schedule_bulk_generate_irn:
+ msg = _('E-Invoices will be generated in a background process.')
+ else:
+ msg = _('E-Invoices will be cancelled in a background process.')
+
+ frappe.msgprint(msg, alert=1)
+
+def check_scheduler_status():
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
+def job_already_enqueued(job_name):
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+ if job_name in enqueued_jobs:
+ return True
\ No newline at end of file
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index f7689cf..ec58fd2 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -51,7 +51,7 @@
def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register',
- 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'):
+ 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
@@ -127,6 +127,9 @@
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
print_hide=1)
+ taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
+ fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
+ print_hide=1)
purchase_invoice_gst_category = [
dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break',
@@ -156,6 +159,13 @@
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 = [
dict(fieldname='invoice_copy', label='Invoice Copy',
fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1,
@@ -280,7 +290,7 @@
'allow_on_submit': 1,
'insert_after': 'customer_name_in_arabic',
'translatable': 0,
- }
+ }
]
si_ewaybill_fields = [
@@ -408,21 +418,37 @@
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
- dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
-
- dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
-
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
- dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+ dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
+ print_hide=1, hidden=1),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
- dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ no_copy=1, print_hide=1),
- dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
+ dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
+ options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
+ hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
]
custom_fields = {
@@ -438,7 +464,7 @@
'Purchase Order': 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,
- '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,
'Tax Category': inter_state_gst_field,
'Item': [
@@ -453,7 +479,7 @@
'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
- 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
+ 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 3637de4..7709ddf 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -2,7 +2,7 @@
import frappe, re, json
from frappe import _
import erpnext
-from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
+from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@@ -41,24 +41,25 @@
return
if len(doc.gstin) != 15:
- frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
+ frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN"))
if gst_category and gst_category == 'UIN Holders':
if not GSTIN_UIN_FORMAT.match(doc.gstin):
- frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
+ frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"),
+ title=_("Invalid GSTIN"))
else:
if not GSTIN_FORMAT.match(doc.gstin):
- frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
+ frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
if not doc.gst_state:
- frappe.throw(_("Please Enter GST state"))
+ frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
if doc.gst_state_number != doc.gstin[:2]:
- frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
- .format(doc.gst_state_number))
+ frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.")
+ .format(doc.gst_state_number), title=_("Invalid GSTIN"))
def validate_pan_for_india(doc, method):
if doc.get('country') != 'India' or not doc.pan:
@@ -832,3 +833,48 @@
account_list.extend(gst_account_list)
return account_list
+
+def update_taxable_values(doc, method):
+ country = frappe.get_cached_value('Company', doc.company, 'country')
+
+ if country != 'India':
+ return
+
+ gst_accounts = get_gst_accounts(doc.company)
+
+ # Only considering sgst account to avoid inflating taxable value
+ gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \
+ + gst_accounts.get('igst_account', [])
+
+ additional_taxes = 0
+ total_charges = 0
+ item_count = 0
+ considered_rows = []
+
+ for tax in doc.get('taxes'):
+ prev_row_id = cint(tax.row_id) - 1
+ if tax.account_head in gst_account_list and prev_row_id not in considered_rows:
+ if tax.charge_type == 'On Previous Row Amount':
+ additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ considered_rows.append(prev_row_id)
+ if tax.charge_type == 'On Previous Row Total':
+ additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total
+ considered_rows.append(prev_row_id)
+
+ for item in doc.get('items'):
+ if doc.apply_discount_on == 'Grand Total' and doc.discount_amount:
+ proportionate_value = item.base_amount if doc.base_total else item.qty
+ total_value = doc.base_total if doc.base_total else doc.total_qty
+ else:
+ proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
+ total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
+
+ applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)),
+ item.precision('taxable_value')))
+ item.taxable_value = applicable_charges + proportionate_value
+ total_charges += applicable_charges
+ item_count += 1
+
+ if total_charges != additional_taxes:
+ diff = additional_taxes - total_charges
+ doc.get('items')[item_count - 1].taxable_value += diff
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index a1f5bb9..7db2f6b 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -139,6 +139,9 @@
dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code',
fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1,
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,
'Sales Order Item': invoice_item_fields,
diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py
index 08573cd..ba1aeaf 100644
--- a/erpnext/regional/italy/utils.py
+++ b/erpnext/regional/italy/utils.py
@@ -57,11 +57,12 @@
invoice.company_address_data = company_address
#Set invoice type
- if invoice.is_return and invoice.return_against:
- invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
- invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
- else:
- invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
+ if not invoice.type_of_document:
+ if invoice.is_return and invoice.return_against:
+ invoice.type_of_document = "TD04" #Credit Note (Nota di Credito)
+ invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against))
+ else:
+ invoice.type_of_document = "TD01" #Sales Invoice (Fattura)
#set customer information
invoice.customer_data = frappe.get_doc("Customer", invoice.customer)
diff --git a/erpnext/selling/doctype/lead_source/__init__.py b/erpnext/regional/report/e_invoice_summary/__init__.py
similarity index 100%
copy from erpnext/selling/doctype/lead_source/__init__.py
copy to erpnext/regional/report/e_invoice_summary/__init__.py
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
new file mode 100644
index 0000000..4713217
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
@@ -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;
+ }
+};
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
new file mode 100644
index 0000000..4deb073
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
new file mode 100644
index 0000000..47acf29
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
@@ -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
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/selling/doctype/lead_source/lead_source.js b/erpnext/selling/doctype/lead_source/lead_source.js
deleted file mode 100644
index 6af6a4f..0000000
--- a/erpnext/selling/doctype/lead_source/lead_source.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Lead Source', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/selling/doctype/lead_source/lead_source.json b/erpnext/selling/doctype/lead_source/lead_source.json
deleted file mode 100644
index 373e83a..0000000
--- a/erpnext/selling/doctype/lead_source/lead_source.json
+++ /dev/null
@@ -1,131 +0,0 @@
-{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "autoname": "field:source_name",
- "beta": 0,
- "creation": "2016-09-16 01:47:47.382372",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "fields": [
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "source_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Source Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "details",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Details",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-09-16 02:03:01.441622",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Lead Source",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
-}
diff --git a/erpnext/selling/doctype/lead_source/test_lead_source.py b/erpnext/selling/doctype/lead_source/test_lead_source.py
deleted file mode 100644
index 42df18f..0000000
--- a/erpnext/selling/doctype/lead_source/test_lead_source.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-# test_records = frappe.get_test_records('Lead Source')
-
-class TestLeadSource(unittest.TestCase):
- pass
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 9e3c9a5..8adf5bf 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -279,11 +279,6 @@
const item_row = frappe.model.get_doc(cdt, cdn);
if (item_row && item_row[fieldname] != value) {
- if (fieldname === 'qty' && flt(value) == 0) {
- this.remove_item_from_cart();
- return;
- }
-
const { item_code, batch_no, uom } = this.item_details.current_item;
const event = {
field: fieldname,
diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
index f396705..6fb7666 100644
--- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
+++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py
@@ -57,18 +57,18 @@
return columns
def get_details(filters):
- conditions = ""
+ sql_query = """SELECT
+ c.name, c.customer_name,
+ ccl.bypass_credit_limit_check,
+ c.is_frozen, c.disabled
+ FROM `tabCustomer` c, `tabCustomer Credit Limit` ccl
+ WHERE
+ c.name = ccl.parent
+ AND ccl.company = %(company)s"""
+
+ # customer filter is optional.
if filters.get("customer"):
- conditions += " AND c.name = '" + filters.get("customer") + "'"
+ sql_query += " AND c.name = %(customer)s"
- return frappe.db.sql("""SELECT
- c.name, c.customer_name,
- ccl.bypass_credit_limit_check,
- c.is_frozen, c.disabled
- FROM `tabCustomer` c, `tabCustomer Credit Limit` ccl
- WHERE
- c.name = ccl.parent
- AND ccl.company = '{0}'
- {1}
- """.format( filters.get("company"),conditions), as_dict=1) #nosec
+ return frappe.db.sql(sql_query, filters, as_dict=1)
diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py
index 0df4c87..933ed3c 100644
--- a/erpnext/setup/doctype/company/delete_company_transactions.py
+++ b/erpnext/setup/doctype/company/delete_company_transactions.py
@@ -27,7 +27,7 @@
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
- "Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
+ "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Mode of Payment Account",
"Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name)
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.js b/erpnext/setup/doctype/global_defaults/global_defaults.js
index 552331a..942dd59 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.js
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.js
@@ -17,7 +17,7 @@
method: "frappe.client.get_list",
args: {
doctype: "UOM Conversion Factor",
- filters: { "category": "Length" },
+ filters: { "category": __("Length") },
fields: ["to_uom"],
limit_page_length: 500
},
diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js
index 1413cb2..885d874 100644
--- a/erpnext/setup/doctype/item_group/item_group.js
+++ b/erpnext/setup/doctype/item_group/item_group.js
@@ -61,7 +61,7 @@
frappe.set_route("List", "Item", {"item_group": frm.doc.name});
});
}
-
+
frappe.model.with_doctype('Item', () => {
const item_meta = frappe.get_meta('Item');
@@ -69,10 +69,12 @@
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
- const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
- field.fieldtype = 'Select';
- field.options = valid_fields;
- frm.fields_dict.filter_fields.grid.refresh();
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'fieldtype', 'Select'
+ );
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'options', valid_fields
+ );
});
},
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py
index cf59a52..d857bf5 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/shopping_cart/test_shopping_cart.py
@@ -16,6 +16,11 @@
Note:
Shopping Cart == Quotation
"""
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
@@ -51,8 +56,8 @@
def test_add_to_cart(self):
self.login_as_customer()
- # remove from cart
- self.remove_all_items_from_cart()
+ # clear existing quotations
+ self.clear_existing_quotations()
# add first item
update_cart("_Test Item", 1)
@@ -100,6 +105,7 @@
self.assertEqual(len(quotation.get("items")), 1)
def test_tax_rule(self):
+ self.create_tax_rule()
self.login_as_customer()
quotation = self.create_quotation()
@@ -115,6 +121,13 @@
self.remove_test_quotation(quotation)
+ def create_tax_rule(self):
+ tax_rule = frappe.get_test_records("Tax Rule")[0]
+ try:
+ frappe.get_doc(tax_rule).insert()
+ except frappe.DuplicateEntryError:
+ pass
+
def create_quotation(self):
quotation = frappe.new_doc("Quotation")
@@ -195,10 +208,15 @@
"_Test Contact For _Test Customer")
frappe.set_user("test_contact_customer@example.com")
- def remove_all_items_from_cart(self):
- quotation = _get_cart_quotation()
- quotation.flags.ignore_permissions=True
- quotation.delete()
+ def clear_existing_quotations(self):
+ quotations = frappe.get_all("Quotation", filters={
+ "party_name": get_party().name,
+ "order_type": "Shopping Cart",
+ "docstatus": 0
+ }, order_by="modified desc", pluck="name")
+
+ for quotation in quotations:
+ frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True)
def create_user_if_not_exists(self, email, first_name = None):
if frappe.db.exists("User", email):
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 95cb92b..933ca8a 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -1,14 +1,14 @@
frappe.provide('erpnext.stock');
erpnext.stock.ItemDashboard = Class.extend({
- init: function(opts) {
+ init: function (opts) {
$.extend(this, opts);
this.make();
},
- make: function() {
+ make: function () {
var me = this;
this.start = 0;
- if(!this.sort_by) {
+ if (!this.sort_by) {
this.sort_by = 'projected_qty';
this.sort_order = 'asc';
}
@@ -16,22 +16,25 @@
this.content = $(frappe.render_template('item_dashboard')).appendTo(this.parent);
this.result = this.content.find('.result');
- this.content.on('click', '.btn-move', function() {
- handle_move_add($(this), "Move")
+ this.content.on('click', '.btn-move', function () {
+ handle_move_add($(this), "Move");
});
- this.content.on('click', '.btn-add', function() {
- handle_move_add($(this), "Add")
+ this.content.on('click', '.btn-add', function () {
+ handle_move_add($(this), "Add");
});
- this.content.on('click', '.btn-edit', function() {
+ this.content.on('click', '.btn-edit', function () {
let item = unescape($(this).attr('data-item'));
let warehouse = unescape($(this).attr('data-warehouse'));
let company = unescape($(this).attr('data-company'));
- frappe.db.get_value('Putaway Rule',
- {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => {
- frappe.set_route("Form", "Putaway Rule", r.name);
- });
+ frappe.db.get_value('Putaway Rule', {
+ 'item_code': item,
+ 'warehouse': warehouse,
+ 'company': company
+ }, 'name', (r) => {
+ frappe.set_route("Form", "Putaway Rule", r.name);
+ });
});
function handle_move_add(element, action) {
@@ -39,23 +42,26 @@
let warehouse = unescape(element.attr('data-warehouse'));
let actual_qty = unescape(element.attr('data-actual_qty'));
let disable_quick_entry = Number(unescape(element.attr('data-disable_quick_entry')));
- let entry_type = action === "Move" ? "Material Transfer": null;
+ let entry_type = action === "Move" ? "Material Transfer" : null;
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr('data-rate'));
- erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function() { me.refresh(); });
- }
- else {
- erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function() { me.refresh(); });
+ erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
+ me.refresh();
+ });
+ } else {
+ erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () {
+ me.refresh();
+ });
}
}
}
function open_stock_entry(item, warehouse, entry_type) {
- frappe.model.with_doctype('Stock Entry', function() {
+ frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry');
if (entry_type) doc.stock_entry_type = entry_type;
@@ -64,18 +70,18 @@
row.s_warehouse = warehouse;
frappe.set_route('Form', doc.doctype, doc.name);
- })
+ });
}
// more
- this.content.find('.btn-more').on('click', function() {
+ this.content.find('.btn-more').on('click', function () {
me.start += me.page_length;
me.refresh();
});
},
- refresh: function() {
- if(this.before_refresh) {
+ refresh: function () {
+ if (this.before_refresh) {
this.before_refresh();
}
@@ -94,13 +100,13 @@
frappe.call({
method: this.method,
args: args,
- callback: function(r) {
+ callback: function (r) {
me.render(r.message);
}
});
},
- render: function(data) {
- if (this.start===0) {
+ render: function (data) {
+ if (this.start === 0) {
this.max_count = 0;
this.result.empty();
}
@@ -115,7 +121,7 @@
this.max_count = this.max_count;
// show more button
- if (data && data.length===(this.page_length + 1)) {
+ if (data && data.length === (this.page_length + 1)) {
this.content.find('.more').removeClass('hidden');
// remove the last element
@@ -137,15 +143,15 @@
}
},
- get_item_dashboard_data: function(data, max_count, show_item) {
- if(!max_count) max_count = 0;
- if(!data) data = [];
+ get_item_dashboard_data: function (data, max_count, show_item) {
+ if (!max_count) max_count = 0;
+ if (!data) data = [];
- data.forEach(function(d) {
+ data.forEach(function (d) {
d.actual_or_pending = d.projected_qty + d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
d.pending_qty = 0;
d.total_reserved = d.reserved_qty + d.reserved_qty_for_production + d.reserved_qty_for_sub_contract;
- if(d.actual_or_pending > d.actual_qty) {
+ if (d.actual_or_pending > d.actual_qty) {
d.pending_qty = d.actual_or_pending - d.actual_qty;
}
@@ -161,16 +167,16 @@
return {
data: data,
max_count: max_count,
- can_write:can_write,
+ can_write: can_write,
show_item: show_item || false
};
},
- get_capacity_dashboard_data: function(data) {
+ get_capacity_dashboard_data: function (data) {
if (!data) data = [];
- data.forEach(function(d) {
- d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef";
+ data.forEach(function (d) {
+ d.color = d.percent_occupied >= 80 ? "#f8814f" : "#2490ef";
});
let can_write = 0;
@@ -185,53 +191,77 @@
}
});
-erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) {
+erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({
title: target ? __('Add Item') : __('Move Item'),
- fields: [
- {fieldname: 'item_code', label: __('Item'),
- fieldtype: 'Link', options: 'Item', read_only: 1},
- {fieldname: 'source', label: __('Source Warehouse'),
- fieldtype: 'Link', options: 'Warehouse', read_only: 1},
- {fieldname: 'target', label: __('Target Warehouse'),
- fieldtype: 'Link', options: 'Warehouse', reqd: 1},
- {fieldname: 'qty', label: __('Quantity'), reqd: 1,
- fieldtype: 'Float', description: __('Available {0}', [actual_qty]) },
- {fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 },
+ fields: [{
+ fieldname: 'item_code',
+ label: __('Item'),
+ fieldtype: 'Link',
+ options: 'Item',
+ read_only: 1
+ },
+ {
+ fieldname: 'source',
+ label: __('Source Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ read_only: 1
+ },
+ {
+ fieldname: 'target',
+ label: __('Target Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ reqd: 1
+ },
+ {
+ fieldname: 'qty',
+ label: __('Quantity'),
+ reqd: 1,
+ fieldtype: 'Float',
+ description: __('Available {0}', [actual_qty])
+ },
+ {
+ fieldname: 'rate',
+ label: __('Rate'),
+ fieldtype: 'Currency',
+ hidden: 1
+ },
],
- })
+ });
dialog.show();
dialog.get_field('item_code').set_input(item);
- if(source) {
+ if (source) {
dialog.get_field('source').set_input(source);
} else {
dialog.get_field('source').df.hidden = 1;
dialog.get_field('source').refresh();
}
- if(rate) {
+ if (rate) {
dialog.get_field('rate').set_value(rate);
dialog.get_field('rate').df.hidden = 0;
dialog.get_field('rate').refresh();
}
- if(target) {
+ if (target) {
dialog.get_field('target').df.read_only = 1;
dialog.get_field('target').value = target;
dialog.get_field('target').refresh();
}
- dialog.set_primary_action(__('Submit'), function() {
+ dialog.set_primary_action(__('Submit'), function () {
var values = dialog.get_values();
- if(!values) {
+ if (!values) {
return;
}
- if(source && values.qty > actual_qty) {
+ if (source && values.qty > actual_qty) {
frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty]));
return;
}
- if(values.source === values.target) {
+ if (values.source === values.target) {
frappe.msgprint(__('Source and target warehouse must be different'));
}
@@ -239,21 +269,21 @@
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
args: values,
freeze: true,
- callback: function(r) {
+ callback: function (r) {
frappe.show_alert(__('Stock Entry {0} created',
- ['<a href="/app/stock-entry/'+r.message.name+'">' + r.message.name+ '</a>']));
+ ['<a href="/app/stock-entry/' + r.message.name + '">' + r.message.name + '</a>']));
dialog.hide();
callback(r);
},
});
});
- $('<p style="margin-left: 10px;"><a class="link-open text-muted small">'
- + __("Add more items or open full form") + '</a></p>')
+ $('<p style="margin-left: 10px;"><a class="link-open text-muted small">' +
+ __("Add more items or open full form") + '</a></p>')
.appendTo(dialog.body)
.find('.link-open')
- .on('click', function() {
- frappe.model.with_doctype('Stock Entry', function() {
+ .on('click', function () {
+ frappe.model.with_doctype('Stock Entry', function () {
var doc = frappe.model.get_new_doc('Stock Entry');
doc.from_warehouse = dialog.get_value('source');
doc.to_warehouse = dialog.get_value('target');
@@ -266,6 +296,6 @@
row.transfer_qty = dialog.get_value('qty');
row.basic_rate = dialog.get_value('rate');
frappe.set_route('Form', doc.doctype, doc.name);
- })
+ });
});
-}
+};
diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py
index cafb5c3..45e6628 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -2,6 +2,7 @@
import frappe
from frappe.model.db_query import DatabaseQuery
+from frappe.utils import flt, cint
@frappe.whitelist()
def get_data(item_code=None, warehouse=None, item_group=None,
@@ -42,11 +43,20 @@
limit_start=start,
limit_page_length='21')
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
+
for item in items:
item.update({
- 'item_name': frappe.get_cached_value("Item", item.item_code, 'item_name'),
- 'disable_quick_entry': frappe.get_cached_value("Item", item.item_code, 'has_batch_no')
- or frappe.get_cached_value("Item", item.item_code, 'has_serial_no'),
+ 'item_name': frappe.get_cached_value(
+ "Item", item.item_code, 'item_name'),
+ 'disable_quick_entry': frappe.get_cached_value(
+ "Item", item.item_code, 'has_batch_no')
+ or frappe.get_cached_value(
+ "Item", item.item_code, 'has_serial_no'),
+ 'projected_qty': flt(item.projected_qty, precision),
+ 'reserved_qty': flt(item.reserved_qty, precision),
+ 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision),
+ 'reserved_qty_for_sub_contract': flt(item.reserved_qty_for_sub_contract, precision),
+ 'actual_qty': flt(item.actual_qty, precision),
})
-
return items
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 36d0de1..e0b89d8 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -494,7 +494,8 @@
test_records = frappe.get_test_records('Item')
-def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=None):
+def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None,
+ customer=None, is_purchase_item=None, opening_stock=None, company=None):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
@@ -509,7 +510,7 @@
item.customer = customer or ''
item.append("item_defaults", {
"default_warehouse": warehouse or '_Test Warehouse - _TC',
- "company": "_Test Company"
+ "company": company or "_Test Company"
})
item.save()
else:
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index c1f20a4..6cec852 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -59,6 +59,8 @@
"show_in_website": 1,
"website_warehouse": "_Test Warehouse - _TC",
"gst_hsn_code": "999800",
+ "opening_stock": 10,
+ "valuation_rate": 100,
"item_defaults": [{
"company": "_Test Company",
"default_warehouse": "_Test Warehouse - _TC",
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
index 24f7e31..e8fb347 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
@@ -15,8 +15,9 @@
}
});
- const child = frappe.meta.get_docfield("Variant Field", "field_name", frm.doc.name);
- child.options = allow_fields;
+ frm.fields_dict.fields.grid.update_docfield_property(
+ 'field_name', 'options', allow_fields
+ );
});
}
});
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js
index bd14e5f..40d4685 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.js
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.js
@@ -110,19 +110,4 @@
refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
}
-var make_row = function(title,val,bold){
- var bstart = '<b>'; var bend = '</b>';
- return '<tr><td class="datalabelcell">'+(bold?bstart:'')+title+(bold?bend:'')+'</td>'
- +'<td class="datainputcell" style="text-align:left;">'+ val +'</td>'
- +'</tr>'
-}
-
-cur_frm.pformat.net_weight_pkg= function(doc){
- return '<table style="width:100%">' + make_row('Net Weight', doc.net_weight_pkg) + '</table>'
-}
-
-cur_frm.pformat.gross_weight_pkg= function(doc){
- return '<table style="width:100%">' + make_row('Gross Weight', doc.gross_weight_pkg) + '</table>'
-}
-
// TODO: validate gross weight field
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 61b7209..6ab68e2 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -346,7 +346,7 @@
if dn_item:
dn_item.warehouse = location.warehouse
- dn_item.qty = location.picked_qty
+ dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
@@ -379,7 +379,6 @@
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
- stock_entry.set_incoming_rate()
stock_entry.set_actual_qty()
stock_entry.calculate_rate_and_amount()
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 8ea7f89d..c4da05a 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -9,6 +9,7 @@
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation \
import EmptyStockReconciliationItemsError
@@ -22,7 +23,7 @@
'purpose': 'Opening Stock',
'expense_account': 'Temporary Opening - _TC',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'warehouse': '_Test Warehouse - _TC',
'valuation_rate': 100,
'qty': 5
@@ -37,7 +38,7 @@
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
'locations': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
@@ -47,7 +48,7 @@
})
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[0].qty, 5)
@@ -237,7 +238,7 @@
'purpose': 'Opening Stock',
'expense_account': 'Temporary Opening - _TC',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'warehouse': '_Test Warehouse - _TC',
'valuation_rate': 100,
'qty': 10
@@ -251,7 +252,7 @@
'customer': '_Test Customer',
'company': '_Test Company',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 10,
'delivery_date': frappe.utils.today()
}],
@@ -264,14 +265,14 @@
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
'locations': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
'sales_order': '_T-Sales Order-1',
'sales_order_item': '_T-Sales Order-1_item',
}, {
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
@@ -281,16 +282,71 @@
})
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item')
- self.assertEqual(pick_list.locations[1].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[1].item_code, '_Test Item')
self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[1].qty, 5)
self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
+ def test_pick_list_for_items_with_multiple_UOM(self):
+ purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10)
+ purchase_receipt.submit()
+
+ sales_order = frappe.get_doc({
+ 'doctype': 'Sales Order',
+ 'customer': '_Test Customer',
+ 'company': '_Test Company',
+ 'items': [{
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'conversion_factor': 5,
+ 'delivery_date': frappe.utils.today()
+ }, {
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'conversion_factor': 1,
+ 'delivery_date': frappe.utils.today()
+ }],
+ }).insert()
+ sales_order.submit()
+
+ pick_list = frappe.get_doc({
+ 'doctype': 'Pick List',
+ 'company': '_Test Company',
+ 'customer': '_Test Customer',
+ 'items_based_on': 'Sales Order',
+ 'locations': [{
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'stock_qty': 5,
+ 'conversion_factor': 5,
+ 'sales_order': sales_order.name,
+ 'sales_order_item': sales_order.items[0].name ,
+ }, {
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ 'sales_order': sales_order.name,
+ 'sales_order_item': sales_order.items[1].name ,
+ }]
+ })
+ pick_list.set_item_locations()
+ pick_list.submit()
+
+ delivery_note = create_delivery_note(pick_list.name)
+
+ self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
+ self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
+ self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
+
+ pick_list.cancel()
+ sales_order.cancel()
+ purchase_receipt.cancel()
# def test_pick_list_skips_items_in_expired_batch(self):
# pass
@@ -302,4 +358,4 @@
# pass
# def test_pick_list_from_material_request(self):
- # pass
\ No newline at end of file
+ # pass
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 57cc350..4d1a514 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -248,13 +248,6 @@
}
}
-cur_frm.cscript.select_print_heading = function(doc, cdt, cdn) {
- if(doc.select_print_heading)
- cur_frm.pformat.print_heading = doc.select_print_heading;
- else
- cur_frm.pformat.print_heading = "Purchase Receipt";
-}
-
cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
return {
filters: [
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f8cfdf8..3f83780 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -124,10 +124,10 @@
return
for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
- check_if_stock_and_account_balance_synced(today(), d.company)
+ check_if_stock_and_account_balance_synced(today(), d.name)
def get_repost_item_valuation_entries():
- date = add_to_date(today(), hours=-12)
+ date = add_to_date(today(), hours=-3)
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
WHERE status != 'Completed' and creation <= %s and docstatus = 1
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 98246fb..42627f9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -856,7 +856,6 @@
}
erpnext.hide_company();
erpnext.utils.add_item(this.frm);
- this.frm.trigger('add_to_transit');
},
scan_barcode: function() {
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 123f0c8..a0e7051 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -179,11 +179,15 @@
def test_material_transfer_gl_entry(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
- mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1",
+ item_code = 'Hand Sanitizer - 001'
+ create_item(item_code =item_code, is_stock_item = 1,
+ is_purchase_item=1, opening_stock=1000, valuation_rate=10, company=company, warehouse="Stores - TCP1")
+
+ mtn = make_stock_entry(item_code=item_code, source="Stores - TCP1",
target="Finished Goods - TCP1", qty=45, company=company)
self.check_stock_ledger_entries("Stock Entry", mtn.name,
- [["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]])
+ [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]])
source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index bddb114..9b90932 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -70,6 +70,7 @@
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
+ "read_only_depends_on": "eval: !doc.__islocal",
"remember_last_selected_value": 1,
"reqd": 1,
"search_index": 1
@@ -244,7 +245,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-02-16 17:21:52.380098",
+ "modified": "2021-04-09 19:54:56.263965",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 9fe12f9..ecc9fcf 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -48,44 +48,62 @@
}
},
- refresh: function (frm) {
- if (frm.doc.status !== "Closed") {
- if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
- frappe.call({
- "method": "frappe.client.get",
- args: {
- doctype: "Service Level Agreement",
- name: frm.doc.service_level_agreement
- },
- callback: function(data) {
- let statuses = data.message.pause_sla_on;
- const hold_statuses = [];
- $.each(statuses, (_i, entry) => {
- hold_statuses.push(entry.status);
- });
- if (hold_statuses.includes(frm.doc.status)) {
- frm.dashboard.clear_headline();
- let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])};
- frm.dashboard.set_headline_alert(
- '<div class="row">' +
- '<div class="col-xs-12">' +
- '<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
- '</div>' +
- '</div>'
- );
- } else {
- set_time_to_resolve_and_response(frm);
- }
- }
- });
- }
+ refresh: function(frm) {
- frm.add_custom_button(__("Close"), function () {
+ // alert messages
+ if (frm.doc.status !== "Closed" && frm.doc.service_level_agreement
+ && frm.doc.agreement_status === "Ongoing") {
+ frappe.call({
+ "method": "frappe.client.get",
+ args: {
+ doctype: "Service Level Agreement",
+ name: frm.doc.service_level_agreement
+ },
+ callback: function(data) {
+ let statuses = data.message.pause_sla_on;
+ const hold_statuses = [];
+ $.each(statuses, (_i, entry) => {
+ hold_statuses.push(entry.status);
+ });
+ if (hold_statuses.includes(frm.doc.status)) {
+ frm.dashboard.clear_headline();
+ let message = { "indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)]) };
+ frm.dashboard.set_headline_alert(
+ '<div class="row">' +
+ '<div class="col-xs-12">' +
+ '<span class="indicator whitespace-nowrap ' + message.indicator + '"><span>' + message.msg + '</span></span> ' +
+ '</div>' +
+ '</div>'
+ );
+ } else {
+ set_time_to_resolve_and_response(frm);
+ }
+ }
+ });
+ } else if (frm.doc.service_level_agreement) {
+ frm.dashboard.clear_headline();
+
+ let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
+ { "indicator": "green", "msg": "Service Level Agreement has been fulfilled" } :
+ { "indicator": "red", "msg": "Service Level Agreement Failed" };
+
+ frm.dashboard.set_headline_alert(
+ '<div class="row">' +
+ '<div class="col-xs-12">' +
+ '<span class="indicator whitespace-nowrap ' + agreement_status.indicator + '"><span class="hidden-xs">' + agreement_status.msg + '</span></span> ' +
+ '</div>' +
+ '</div>'
+ );
+ }
+
+ // buttons
+ if (frm.doc.status !== "Closed") {
+ frm.add_custom_button(__("Close"), function() {
frm.set_value("status", "Closed");
frm.save();
});
- frm.add_custom_button(__("Task"), function () {
+ frm.add_custom_button(__("Task"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.support.doctype.issue.issue.make_task",
frm: frm
@@ -93,23 +111,7 @@
}, __("Create"));
} else {
- if (frm.doc.service_level_agreement) {
- frm.dashboard.clear_headline();
-
- let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
- {"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} :
- {"indicator": "red", "msg": "Service Level Agreement Failed"};
-
- frm.dashboard.set_headline_alert(
- '<div class="row">' +
- '<div class="col-xs-12">' +
- '<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
- '</div>' +
- '</div>'
- );
- }
-
- frm.add_custom_button(__("Reopen"), function () {
+ frm.add_custom_button(__("Reopen"), function() {
frm.set_value("status", "Open");
frm.save();
});
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 767a8a6..b068363 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -7,7 +7,7 @@
from frappe import _
from frappe import utils
from frappe.model.document import Document
-from frappe.utils import now_datetime, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds
+from frappe.utils import cint, now_datetime, getdate, get_weekdays, add_to_date, get_time, get_datetime, time_diff_in_seconds
from datetime import datetime, timedelta
from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import is_website_user
@@ -128,8 +128,8 @@
def update_agreement_status(self):
if self.service_level_agreement and self.agreement_status == "Ongoing":
- if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \
- frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0:
+ if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \
+ cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0:
self.agreement_status = "Failed"
else:
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index 5346195..00060b9 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -10,7 +10,9 @@
let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options;
statuses = statuses.split('\n');
allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status));
- frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses);
+ frm.fields_dict.pause_sla_on.grid.update_docfield_property(
+ 'status', 'options', [''].concat(allow_statuses)
+ );
});
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js
index f87b2c2..746eee0 100644
--- a/erpnext/support/report/issue_analytics/issue_analytics.js
+++ b/erpnext/support/report/issue_analytics/issue_analytics.js
@@ -52,6 +52,7 @@
label: __("Status"),
fieldtype: "Select",
options:[
+ "",
{label: __('Open'), value: 'Open'},
{label: __('Replied'), value: 'Replied'},
{label: __('Resolved'), value: 'Resolved'},
@@ -138,4 +139,4 @@
}
});
}
-};
\ No newline at end of file
+};
diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js
index 684482a..eb0e06c 100644
--- a/erpnext/support/report/issue_summary/issue_summary.js
+++ b/erpnext/support/report/issue_summary/issue_summary.js
@@ -39,6 +39,7 @@
label: __("Status"),
fieldtype: "Select",
options:[
+ "",
{label: __('Open'), value: 'Open'},
{label: __('Replied'), value: 'Replied'},
{label: __('Resolved'), value: 'Resolved'},
@@ -70,4 +71,4 @@
options: "User"
}
]
-};
\ No newline at end of file
+};