Merge branch 'develop' into employee-util-report
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 199a183..d2e5870 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.0.0-dev'
+__version__ = '13.0.1'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index c801cfc..0606823 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -214,6 +214,7 @@
if parent_value_changed:
doc.save()
+ @frappe.whitelist()
def convert_group_to_ledger(self):
if self.check_if_child_exists():
throw(_("Account with child nodes cannot be converted to ledger"))
@@ -224,6 +225,7 @@
self.save()
return 1
+ @frappe.whitelist()
def convert_ledger_to_group(self):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index df6cedd..63b5dbb 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -39,6 +39,7 @@
frappe.throw(_("Accounting Period overlaps with {0}")
.format(existing_accounting_period[0].get("name")), OverlapError)
+ @frappe.whitelist()
def get_doctypes_for_closing(self):
docs_for_closing = []
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
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_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 76d82e7..79f5596 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -12,6 +12,7 @@
}
class BankClearance(Document):
+ @frappe.whitelist()
def get_payment_entries(self):
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
@@ -108,6 +109,7 @@
row.update(d)
self.total_amount += flt(amount)
+ @frappe.whitelist()
def update_clearance_date(self):
clearance_date_updated = False
for d in self.get('payment_entries'):
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index ad4ff9e..3dbd605 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -532,43 +532,4 @@
</table>
`);
},
-
- show_missing_link_values(frm, missing_link_values) {
- let can_be_created_automatically = missing_link_values.every(
- (d) => d.has_one_mandatory_field
- );
-
- let html = missing_link_values
- .map((d) => {
- let doctype = d.doctype;
- let values = d.missing_values;
- return `
- <h5>${doctype}</h5>
- <ul>${values.map((v) => `<li>${v}</li>`).join("")}</ul>
- `;
- })
- .join("");
-
- if (can_be_created_automatically) {
- // prettier-ignore
- let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
- frappe.confirm(message + html, () => {
- frm.call("create_missing_link_values", {
- missing_link_values,
- }).then((r) => {
- let records = r.message;
- frappe.msgprint(__(
- "Created {0} records successfully.", [
- records.length,
- ]
- ));
- });
- });
- } else {
- frappe.msgprint(
- // prettier-ignore
- __('The following records needs to be created before we can import your file.') + html
- );
- }
- },
});
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 3b14e4e..ce149f9 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -15,12 +15,14 @@
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
make_pos_profile()
add_transactions()
add_vouchers()
- def tearDown(self):
+ @classmethod
+ def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel()
@@ -33,9 +35,6 @@
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
- frappe.flags.test_bank_transactions_created = False
- frappe.flags.test_payments_created = False
-
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
@@ -44,8 +43,8 @@
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
- payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
+ bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
+ payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps([{
"payment_doctype":"Payment Entry",
"payment_name":payment.name,
@@ -62,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
@@ -116,10 +114,6 @@
pass
def add_transactions():
- if frappe.flags.test_bank_transactions_created:
- return
-
- frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({
@@ -172,14 +166,8 @@
}).insert()
doc.submit()
- frappe.flags.test_bank_transactions_created = True
def add_vouchers():
- if frappe.flags.test_payments_created:
- return
-
- frappe.set_user("Administrator")
-
try:
frappe.get_doc({
"doctype": "Supplier",
@@ -272,13 +260,6 @@
except frappe.DuplicateEntryError:
pass
- si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
- pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
- pe.reference_no = "Fayva Oct 18"
- pe.reference_date = "2018-10-29"
- pe.insert()
- pe.submit()
-
mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment",
"name": "Cash"
@@ -291,14 +272,12 @@
})
mode_of_payment.save()
- si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1)
+ si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
si.append("payments", {
"mode_of_payment": "Cash",
"account": "_Test Bank - _TC",
"amount": 109080
})
- si.save()
+ si.insert()
si.submit()
-
- frappe.flags.test_payments_created = True
diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py
index 9b64f81..fd86ed4 100644
--- a/erpnext/accounts/doctype/c_form/c_form.py
+++ b/erpnext/accounts/doctype/c_form/c_form.py
@@ -57,6 +57,7 @@
total = sum([flt(d.grand_total) for d in self.get('invoices')])
frappe.db.set(self, 'total_invoiced_amount', total)
+ @frappe.whitelist()
def get_invoice_details(self, invoice_no):
""" Pull details from invoices for referrence """
if invoice_no:
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/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py
index 12094d4..8a5473f 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center.py
@@ -50,6 +50,7 @@
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center)))
+ @frappe.whitelist()
def convert_group_to_ledger(self):
if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
@@ -60,6 +61,7 @@
self.save()
return 1
+ @frappe.whitelist()
def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center):
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 9594706..c1b8ba7 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -27,6 +27,7 @@
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
+ @frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []
self.validate_mandatory()
@@ -95,6 +96,7 @@
message = _("No outstanding invoices found")
frappe.msgprint(message)
+ @frappe.whitelist()
def make_jv_entry(self):
if self.total_gain_loss == 0:
return
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index da6a3fd..4255626 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -12,6 +12,7 @@
class FiscalYearIncorrectDate(frappe.ValidationError): pass
class FiscalYear(Document):
+ @frappe.whitelist()
def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults")
@@ -54,7 +55,7 @@
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
-
+
def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index ce76d0a..78febf9 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -290,4 +290,8 @@
oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name
- frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname))
+ frappe.db.sql(
+ "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
+ (newname, oldname),
+ auto_commit=True
+ )
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index af8940c..7b62b61 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -125,6 +125,7 @@
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
+ @frappe.whitelist()
def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry'
@@ -174,6 +175,7 @@
return je
+ @frappe.whitelist()
def close_loan(self):
je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry'
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/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 3419bb6..ff2c8c2 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -564,6 +564,7 @@
if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
+ @frappe.whitelist()
def get_balance(self):
if not self.get('accounts'):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 774159d..a89fefd 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -280,7 +280,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-06-24 14:06:54.833738",
+ "modified": "2020-06-26 14:06:54.833738",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
index 18f853c..88667d7 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
@@ -8,6 +8,7 @@
from frappe.model.document import Document
class MonthlyDistribution(Document):
+ @frappe.whitelist()
def get_months(self):
month_list = ['January','February','March','April','May','June','July','August','September',
'October','November','December']
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index e6449b7..29dc96e 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -167,6 +167,7 @@
return invoice
+ @frappe.whitelist()
def make_invoices(self):
self.validate_company()
invoices = self.get_invoices()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index b5f6a40..c2e804e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -637,13 +637,13 @@
let to_field = fields[key][1];
if (filters[from_field] && !filters[to_field]) {
- frappe.throw(__("Error: {0} is mandatory field",
- [to_field.replace(/_/g, " ")]
- ));
+ frappe.throw(
+ __("Error: {0} is mandatory field", [to_field.replace(/_/g, " ")])
+ );
} else if (filters[from_field] && filters[from_field] > filters[to_field]) {
- frappe.throw(__("{0}: {1} must be less than {2}",
- [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")]
- ));
+ frappe.throw(
+ __("{0}: {1} must be less than {2}", [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")])
+ );
}
}
},
@@ -692,6 +692,8 @@
c.total_amount = d.invoice_amount;
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
+ c.payment_term = d.payment_term;
+ c.allocated_amount = d.allocated_amount;
if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) {
if(flt(d.outstanding_amount) > 0)
@@ -774,12 +776,15 @@
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) {
- frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice",
- [frm.doc.payment_type,
- (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type]));
+ frappe.msgprint(
+ __("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
+ (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
+ );
return false
} else {
- frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding]));
+ frappe.msgprint(
+ __("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
+ );
return false;
}
} else {
@@ -791,10 +796,13 @@
}
$.each(frm.doc.references || [], function(i, row) {
- row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount
- if(frappe.flags.allocate_payment_amount != 0){
- if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
- if(row.outstanding_amount >= allocated_positive_outstanding) {
+ if (frappe.flags.allocate_payment_amount == 0) {
+ //If allocate payment amount checkbox is unchecked, set zero to allocate amount
+ row.allocated_amount = 0;
+
+ } else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
+ if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
+ if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
@@ -802,9 +810,11 @@
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
- if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding)
+ if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
- else row.allocated_amount = row.outstanding_amount;
+ } else {
+ row.allocated_amount = row.outstanding_amount;
+ };
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 8acd92c..62ab76c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -333,33 +333,50 @@
invoice_payment_amount_map = {}
invoice_paid_amount_map = {}
- for reference in self.get('references'):
- if reference.payment_term and reference.reference_name:
- key = (reference.payment_term, reference.reference_name)
+ for ref in self.get('references'):
+ if ref.payment_term and ref.reference_name:
+ key = (ref.payment_term, ref.reference_name)
invoice_payment_amount_map.setdefault(key, 0.0)
- invoice_payment_amount_map[key] += reference.allocated_amount
+ invoice_payment_amount_map[key] += ref.allocated_amount
if not invoice_paid_amount_map.get(key):
- payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name},
- fields=['paid_amount', 'payment_amount', 'payment_term'])
+ payment_schedule = frappe.get_all(
+ 'Payment Schedule',
+ filters={'parent': ref.reference_name},
+ fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding']
+ )
for term in payment_schedule:
- invoice_key = (term.payment_term, reference.reference_name)
+ invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {})
- invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount
+ invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
+ invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
- for key, amount in iteritems(invoice_payment_amount_map):
+ for key, allocated_amount in iteritems(invoice_payment_amount_map):
+ outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
+ discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
+
if cancel:
- frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s
- WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+ frappe.db.sql("""
+ UPDATE `tabPayment Schedule`
+ SET
+ paid_amount = `paid_amount` - %s,
+ discounted_amount = `discounted_amount` - %s,
+ outstanding = `outstanding` + %s
+ WHERE parent = %s and payment_term = %s""",
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
else:
- outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
-
- if amount > outstanding:
+ if allocated_amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
- if amount and outstanding:
- frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
- WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+ if allocated_amount and outstanding:
+ frappe.db.sql("""
+ UPDATE `tabPayment Schedule`
+ SET
+ paid_amount = `paid_amount` + %s,
+ discounted_amount = `discounted_amount` + %s,
+ outstanding = `outstanding` - %s
+ WHERE parent = %s and payment_term = %s""",
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
def set_status(self):
if self.docstatus == 2:
@@ -708,6 +725,8 @@
outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
args.get("party_account"), filters=args, condition=condition)
+ outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
+
for d in outstanding_invoices:
d["exchange_rate"] = 1
if party_account_currency != company_currency:
@@ -735,6 +754,46 @@
return data
+def split_invoices_based_on_payment_terms(outstanding_invoices):
+ invoice_ref_based_on_payment_terms = {}
+ for idx, d in enumerate(outstanding_invoices):
+ if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']:
+ payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template')
+ if payment_term_template:
+ allocate_payment_based_on_payment_terms = frappe.db.get_value(
+ 'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms')
+ if allocate_payment_based_on_payment_terms:
+ payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"])
+
+ for payment_term in payment_schedule:
+ if payment_term.outstanding > 0.1:
+ invoice_ref_based_on_payment_terms.setdefault(idx, [])
+ invoice_ref_based_on_payment_terms[idx].append(frappe._dict({
+ 'due_date': d.due_date,
+ 'currency': d.currency,
+ 'voucher_no': d.voucher_no,
+ 'voucher_type': d.voucher_type,
+ 'posting_date': d.posting_date,
+ 'invoice_amount': flt(d.invoice_amount),
+ 'outstanding_amount': flt(d.outstanding_amount),
+ 'payment_amount': payment_term.payment_amount,
+ 'payment_term': payment_term.payment_term,
+ 'allocated_amount': payment_term.outstanding
+ }))
+
+ if invoice_ref_based_on_payment_terms:
+ for idx, ref in invoice_ref_based_on_payment_terms.items():
+ voucher_no = outstanding_invoices[idx]['voucher_no']
+ voucher_type = outstanding_invoices[idx]['voucher_type']
+
+ frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
+ voucher_type, voucher_no, len(ref)), alert=True)
+
+ outstanding_invoices.pop(idx - 1)
+ outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
+
+ return outstanding_invoices
+
def get_orders_to_be_billed(posting_date, party_type, party,
company, party_account_currency, company_currency, cost_center=None, filters=None):
if party_type == "Customer":
@@ -1091,6 +1150,8 @@
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
+ paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc)
+
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
pe.company = doc.company
@@ -1160,11 +1221,20 @@
pe.setup_party_account_field()
pe.set_missing_values()
+
if party_account and bank:
if dt == "Employee Advance":
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
+ if discount_amount:
+ pe.set_gain_or_loss(account_details={
+ 'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"),
+ 'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"),
+ 'amount': discount_amount * (-1 if payment_type == "Pay" else 1)
+ })
+ pe.set_difference_amount()
+
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1285,6 +1355,33 @@
paid_amount = received_amount * doc.get('exchange_rate', 1)
return paid_amount, received_amount
+def apply_early_payment_discount(paid_amount, received_amount, doc):
+ total_discount = 0
+ if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
+ for term in doc.payment_schedule:
+ if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
+ if term.discount_type == 'Percentage':
+ discount_amount = flt(doc.get('grand_total')) * (term.discount / 100)
+ else:
+ discount_amount = term.discount
+
+ discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
+
+ if doc.doctype == 'Sales Invoice':
+ paid_amount -= discount_amount
+ received_amount -= discount_amount_in_foreign_currency
+ else:
+ received_amount -= discount_amount
+ paid_amount -= discount_amount_in_foreign_currency
+
+ total_discount += discount_amount
+
+ if total_discount:
+ money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency'))
+ frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
+
+ return paid_amount, received_amount, total_discount
+
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = []
for payment_term in payment_schedule:
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 772fc1a..4641d6b 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -193,6 +193,34 @@
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
+ def test_payment_entry_against_payment_terms_with_discount(self):
+ si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+ create_payment_terms_template_with_discount()
+ si.payment_terms_template = 'Test Discount Template'
+
+ frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC')
+
+ si.append('taxes', {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18
+ })
+ si.save()
+
+ si.submit()
+
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ pe.submit()
+ si.load_from_db()
+
+ self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount')
+ self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
+ self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
+ self.assertEqual(si.payment_schedule[0].outstanding, 0)
+ self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
+
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
@@ -591,6 +619,26 @@
}]
}).insert()
+def create_payment_terms_template_with_discount():
+
+ create_payment_term('30 Credit Days with 10% Discount')
+
+ if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
+ payment_term_template = frappe.get_doc({
+ 'doctype': 'Payment Terms Template',
+ 'template_name': 'Test Discount Template',
+ 'allocate_payment_based_on_payment_terms': 1,
+ 'terms': [{
+ 'doctype': 'Payment Terms Template Detail',
+ 'payment_term': '30 Credit Days with 10% Discount',
+ 'invoice_portion': 100,
+ 'credit_days_based_on': 'Day(s) after invoice date',
+ 'credit_days': 2,
+ 'discount': 10,
+ 'discount_validity_based_on': 'Day(s) after invoice date',
+ 'discount_validity': 1
+ }]
+ }).insert()
def create_payment_term(name):
if not frappe.db.exists('Payment Term', name):
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 8f5e9fb..912ad09 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -58,7 +58,7 @@
"fieldname": "total_amount",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Total Amount",
+ "label": "Grand Total",
"print_hide": 1,
"read_only": 1
},
@@ -92,9 +92,10 @@
"options": "Payment Term"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-03-13 12:07:19.362539",
+ "modified": "2021-02-10 11:25:47.144392",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
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/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index f7a15c0..cf6ec18 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -11,6 +11,7 @@
from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document):
+ @frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
self.get_invoice_entries()
@@ -147,6 +148,7 @@
ent.currency = e.get('currency')
ent.outstanding_amount = e.get('outstanding_amount')
+ @frappe.whitelist()
def reconcile(self, args):
for e in self.get('payments'):
e.invoice_type = None
@@ -197,6 +199,7 @@
'difference_account': row.difference_account
})
+ @frappe.whitelist()
def get_difference_amount(self, child_row):
if child_row.get("reference_type") != 'Payment Entry': return
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
index d363cf1..e362566 100644
--- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
@@ -6,11 +6,23 @@
"engine": "InnoDB",
"field_order": [
"payment_term",
+ "section_break_15",
"description",
+ "section_break_4",
"due_date",
- "invoice_portion",
- "payment_amount",
"mode_of_payment",
+ "column_break_5",
+ "invoice_portion",
+ "section_break_6",
+ "discount_type",
+ "discount_date",
+ "column_break_9",
+ "discount",
+ "section_break_9",
+ "payment_amount",
+ "discounted_amount",
+ "column_break_3",
+ "outstanding",
"paid_amount"
],
"fields": [
@@ -25,6 +37,7 @@
},
{
"columns": 2,
+ "fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@@ -62,14 +75,82 @@
"options": "Mode of Payment"
},
{
+ "depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "discounted_amount",
+ "fieldname": "discounted_amount",
+ "fieldtype": "Currency",
+ "label": "Discounted Amount",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "payment_amount",
+ "fieldname": "outstanding",
+ "fieldtype": "Currency",
+ "label": "Outstanding",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "discount",
+ "fieldname": "discount_date",
+ "fieldtype": "Date",
+ "label": "Discount Date",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "default": "Percentage",
+ "fetch_from": "payment_term.discount_type",
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fetch_from": "payment_term.discount",
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_15",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-03-13 17:58:24.729526",
+ "modified": "2021-02-15 21:03:12.540546",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js
index 054c2d1..acd0144 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term.js
+++ b/erpnext/accounts/doctype/payment_term/payment_term.js
@@ -1,2 +1,22 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.ui.form.on('Payment Term', {
+ onload(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ discount(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ discount_type(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ set_dynamic_description(frm) {
+ if (frm.doc.discount) {
+ let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
+ if (frm.doc.discount_type == 'Amount') {
+ description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]);
+ }
+ frm.set_df_property("discount", "description", description);
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.json b/erpnext/accounts/doctype/payment_term/payment_term.json
index e77c244..aec4965 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term.json
+++ b/erpnext/accounts/doctype/payment_term/payment_term.json
@@ -1,386 +1,166 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:payment_term_name",
- "beta": 0,
- "creation": "2017-08-10 15:24:54.876365",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:payment_term_name",
+ "creation": "2017-08-10 15:24:54.876365",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_term_name",
+ "invoice_portion",
+ "mode_of_payment",
+ "column_break_3",
+ "due_date_based_on",
+ "credit_days",
+ "credit_months",
+ "section_break_8",
+ "discount_type",
+ "discount",
+ "column_break_11",
+ "discount_validity_based_on",
+ "discount_validity",
+ "section_break_6",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_term_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Term Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "payment_term_name",
+ "fieldtype": "Data",
+ "label": "Payment Term Name",
+ "unique": 1
+ },
{
- "description": "Provide the invoice portion in percent",
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice_portion",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Invoice Portion",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Float",
+ "label": "Invoice Portion (%)"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "due_date_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Due Date Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "label": "Due Date Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
{
- "description": "Give number of days according to prior selection",
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "label": "Credit Days"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
- "fieldname": "credit_months",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Months",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "label": "Credit Months"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "bold": 1,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break",
+ "label": "Discount Settings"
+ },
+ {
+ "default": "Percentage",
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "default": "Day(s) after invoice date",
+ "depends_on": "discount",
+ "fieldname": "discount_validity_based_on",
+ "fieldtype": "Select",
+ "label": "Discount Validity Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
+ {
+ "depends_on": "discount",
+ "fieldname": "discount_validity",
+ "fieldtype": "Int",
+ "label": "Discount Validity",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
}
- ],
- "has_web_view": 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-10-14 10:47:32.830478",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Term",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2021-02-15 20:30:56.256403",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Term",
+ "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": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "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": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "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": "Accounts User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
index f5c5bca..84c8d09 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
@@ -3,11 +3,6 @@
frappe.ui.form.on('Payment Terms Template', {
setup: function(frm) {
- frm.add_fetch("payment_term", "description", "description");
- frm.add_fetch("payment_term", "invoice_portion", "invoice_portion");
- frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on");
- frm.add_fetch("payment_term", "credit_days", "credit_days");
- frm.add_fetch("payment_term", "credit_months", "credit_months");
- frm.add_fetch("payment_term", "mode_of_payment", "mode_of_payment");
+
}
});
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
index 2b2b6af..80e3348 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -13,7 +13,6 @@
class PaymentTermsTemplate(Document):
def validate(self):
self.validate_invoice_portion()
- self.validate_credit_days()
self.check_duplicate_terms()
def validate_invoice_portion(self):
@@ -24,11 +23,6 @@
if flt(total_portion, 2) != 100.00:
frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
- def validate_credit_days(self):
- for term in self.terms:
- if cint(term.credit_days) < 0:
- frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red')
-
def check_duplicate_terms(self):
terms = []
for term in self.terms:
diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
index eee3223..20b3dca 100644
--- a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
+++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
@@ -1,278 +1,164 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-08-10 15:34:09.409562",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-08-10 15:34:09.409562",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_term",
+ "section_break_13",
+ "description",
+ "section_break_4",
+ "invoice_portion",
+ "mode_of_payment",
+ "column_break_3",
+ "due_date_based_on",
+ "credit_days",
+ "credit_months",
+ "section_break_8",
+ "discount_type",
+ "discount",
+ "column_break_11",
+ "discount_validity_based_on",
+ "discount_validity"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "payment_term",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Term",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payment Term",
+ "options": "Payment Term"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "default": "0",
- "fieldname": "invoice_portion",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Invoice Portion",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.invoice_portion",
+ "fetch_if_empty": 1,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Invoice Portion (%)",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "due_date_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Due Date Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.due_date_based_on",
+ "fetch_if_empty": 1,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Due Date Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "default": "0",
- "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "default": "0",
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fetch_from": "payment_term.credit_days",
+ "fetch_if_empty": 1,
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Credit Days",
+ "non_negative": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
- "fieldname": "credit_months",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Months",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fetch_from": "payment_term.credit_months",
+ "fetch_if_empty": 1,
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "label": "Credit Months",
+ "non_negative": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fetch_from": "payment_term.mode_of_payment",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break",
+ "label": "Discount Settings"
+ },
+ {
+ "default": "Percentage",
+ "fetch_from": "payment_term.discount_type",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fetch_from": "payment_term.discount",
+ "fetch_if_empty": 1,
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Day(s) after invoice date",
+ "depends_on": "discount",
+ "fetch_from": "payment_term.discount_validity_based_on",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_validity_based_on",
+ "fieldtype": "Select",
+ "label": "Discount Validity Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "depends_on": "discount",
+ "fetch_from": "payment_term.discount_validity",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_validity",
+ "fieldtype": "Int",
+ "label": "Discount Validity",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:55.143025",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Terms Template Detail",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-24 11:56:12.410807",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Terms Template Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index f5224a2..a05e598 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -18,7 +18,7 @@
self.validate_pos_closing()
self.validate_pos_invoices()
-
+
def validate_pos_closing(self):
user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry`
@@ -37,12 +37,12 @@
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
-
+
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:
invalid_row = {'idx': d.idx}
- pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
+ pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
if pos_invoice.consolidated_invoice:
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
@@ -68,14 +68,15 @@
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
+ @frappe.whitelist()
def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency})
-
+
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
-
+
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 402d157..e614459 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -108,7 +108,6 @@
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
if d.batch_no:
filters["batch_no"] = d.batch_no
-
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
serial_nos = get_serial_nos(d.serial_no)
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
@@ -355,6 +354,7 @@
return profile
+ @frappe.whitelist()
def set_missing_values(self, for_validate=False):
profile = self.set_pos_fields(for_validate)
@@ -377,6 +377,7 @@
"allow_print_before_pay": profile.get("allow_print_before_pay")
}
+ @frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile)
@@ -389,6 +390,7 @@
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
+ @frappe.whitelist()
def create_payment_request(self):
for pay in self.payments:
if pay.type == "Phone":
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 054afe5..6d388c4 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -12,6 +12,10 @@
from erpnext.stock.doctype.item.test_item import make_item
class TestPOSInvoice(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
def tearDown(self):
if frappe.session.user != "Administrator":
frappe.set_user("Administrator")
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_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index ee76bba..cf7ed26 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -62,14 +62,15 @@
if len(default_mode) > 1:
frappe.throw(_("You can only select one mode of payment as default"))
-
+
invalid_modes = []
for d in self.payments:
account = frappe.db.get_value(
- "Mode of Payment Account",
+ "Mode of Payment Account",
{"parent": d.mode_of_payment, "company": self.company},
"default_account"
)
+
if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index 62dc1fc..0033965 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -92,11 +92,21 @@
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
})
- payments = [{
+ mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
+ company = args.company or "_Test Company"
+ default_account = args.income_account or "Sales - _TC"
+
+ if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}):
+ mode_of_payment.append("accounts", {
+ "company": company,
+ "default_account": default_account
+ })
+ mode_of_payment.save()
+
+ pos_profile.append("payments", {
'mode_of_payment': 'Cash',
'default': 1
- }]
- pos_profile.set("payments", payments)
+ })
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
pos_profile.insert()
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/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 7425132..6dc4643 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -92,7 +92,7 @@
frm.refresh_field('customers');
}
else{
- frappe.msgprint('No Customers found with selected options.');
+ frappe.throw('No Customers found with selected options.');
}
}
}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index d50e4a8..43fbb06 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -126,9 +126,11 @@
sales_person_records = frappe._dict()
for d in records:
sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
- customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
+ if sales_person_records.get('Customer'):
+ return frappe.get_list('Customer', fields=['name', 'email_id'], \
filters=[['name', 'in', list(sales_person_records['Customer'])]])
- return customers
+ else:
+ return []
def get_recipients_and_cc(customer, doc):
recipients = []
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..2d5760b 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 22:45:58.334107",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json
index e7166c5..9f9e90d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_records.json
+++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json
@@ -43,7 +43,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-BILL",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -167,7 +167,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-Purchase Invoice-",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
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/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 720a917..d382386 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1952,13 +1952,12 @@
"is_submittable": 1,
"links": [
{
- "custom": 1,
"group": "Reference",
"link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-02-01 15:42:26.261540",
+ "modified": "2021-03-31 15:42:26.261540",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index a1bf66b..3c91dcc 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -24,6 +24,7 @@
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -214,6 +215,9 @@
if self.update_stock == 1:
self.repost_future_sle_and_gle()
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
@@ -390,6 +394,7 @@
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
+ @frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@@ -729,6 +734,7 @@
else:
self.calculate_billing_amount_for_timesheet()
+ @frappe.whitelist()
def add_timesheet_data(self):
self.set('timesheets', [])
if self.project:
@@ -1286,6 +1292,7 @@
break
# Healthcare
+ @frappe.whitelist()
def set_healthcare_services(self, checked_values):
self.set("items", [])
from erpnext.stock.get_item_details import get_item_details
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index e00a58f..3781f8c 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -31,7 +31,7 @@
"base_grand_total": 561.8,
"grand_total": 561.8,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -104,7 +104,7 @@
"base_grand_total": 630.0,
"grand_total": 630.0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -175,7 +175,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -301,7 +301,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Excise Duty - _TC",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 90e2144..4a6f9d1 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1166,10 +1166,12 @@
def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
- price_list_rate = 100
+ price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = 'Percentage'
si.items[0].margin_rate_or_amount = 25
+ si.items[0].discount_amount = 0.0
+ si.items[0].discount_percentage = 0.0
si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))
@@ -2115,6 +2117,7 @@
si.return_against = args.return_against
si.currency=args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
+ si.naming_series = args.naming_series or "T-SINV-"
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
index 632e30d..ac1ffd9 100644
--- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
@@ -14,10 +14,15 @@
from six import iteritems
class TestTaxRule(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
+ frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0)
+
+ @classmethod
+ def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
- def tearDown(self):
+ def setUp(self):
frappe.db.sql("delete from `tabTax Rule`")
def test_conflict(self):
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 51fc7ec..444b40e 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -364,7 +364,7 @@
payment_terms_details = frappe.db.sql("""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
- ps.due_date, ps.payment_amount, ps.description, ps.paid_amount
+ ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -395,13 +395,13 @@
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description,
- "paid": d.paid_amount,
+ "paid": d.paid_amount + d.discounted_amount,
"credit_note": 0.0,
- "outstanding": invoiced - d.paid_amount
+ "outstanding": invoiced - d.paid_amount - d.discounted_amount
}))
if d.paid_amount:
- row['paid'] -= d.paid_amount
+ row['paid'] -= d.paid_amount + d.discounted_amount
def allocate_closing_to_term(self, row, term, key):
if row[key]:
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/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index fadb665..9ffa481 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -444,6 +444,16 @@
"type": "Link"
},
{
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "UAE VAT 201",
+ "link_to": "UAE VAT 201",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
"hidden": 0,
"is_query_report": 0,
"label": "Financial Statements",
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index afbd9b4..9000dea 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -71,6 +71,7 @@
"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
}).insert()
+ @frappe.whitelist()
def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
required_fields = ['location', 'name', 'collection_datetime']
@@ -87,6 +88,7 @@
frappe.publish_realtime("List of Linked Docs",
output, user=frappe.session.user)
+ @frappe.whitelist()
def append_to_child(self, obj_to_append):
for doctype in obj_to_append:
for doc_name in set(obj_to_append[doctype]):
diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py
index dc2781c..9cb492a 100644
--- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py
+++ b/erpnext/agriculture/doctype/fertilizer/fertilizer.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class Fertilizer(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
index 304727e..2806cc6 100644
--- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
+++ b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
@@ -8,6 +8,7 @@
from frappe.model.document import Document
class PlantAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
index 17b96a0..37835f8 100644
--- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
+++ b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class SoilAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py
index 8c1d7ed..209b2c8 100644
--- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py
+++ b/erpnext/agriculture/doctype/soil_texture/soil_texture.py
@@ -13,6 +13,7 @@
soil_edit_order = [2, 1, 0]
soil_types = ['clay_composition', 'sand_composition', 'silt_composition']
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'})
for doc in docs:
@@ -26,6 +27,7 @@
if sum(self.get(soil_type) for soil_type in self.soil_types) != 100:
frappe.throw(_('Soil compositions do not add up to 100'))
+ @frappe.whitelist()
def update_soil_edit(self, soil_type):
self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1
self.soil_type = self.get_soil_type()
@@ -35,8 +37,8 @@
if sum(self.soil_edit_order) < 5: return
last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order))
- # set composition of the last edited soil
- self.set( self.soil_types[last_edit_index],
+ # set composition of the last edited soil
+ self.set(self.soil_types[last_edit_index],
100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index])))
# calculate soil type
@@ -67,4 +69,4 @@
elif (c >= 40 and sa <= 45 and si < 40):
return 'Clay'
else:
- return 'Select'
\ No newline at end of file
+ return 'Select'
diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py
index 88f1fbd..d9f007c 100644
--- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py
+++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py
@@ -9,11 +9,13 @@
from frappe import _
class WaterAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'})
for doc in docs:
self.append('water_analysis_criteria', {'title': str(doc.name)})
+ @frappe.whitelist()
def update_lab_result_date(self):
if not self.result_datetime:
self.result_datetime = self.laboratory_testing_datetime
diff --git a/erpnext/agriculture/doctype/weather/weather.py b/erpnext/agriculture/doctype/weather/weather.py
index 938daa2..235e684 100644
--- a/erpnext/agriculture/doctype/weather/weather.py
+++ b/erpnext/agriculture/doctype/weather/weather.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class Weather(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'})
for doc in docs:
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index e8e8ec6..9aff144 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -553,6 +553,7 @@
make_gl_entries(gl_entries)
self.db_set('booked_fixed_asset', 1)
+ @frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, string_types):
args = json.loads(args)
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 248cb9a..630a1dc 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -13,6 +13,8 @@
"po_required",
"pr_required",
"maintain_same_rate",
+ "maintain_same_rate_action",
+ "role_to_override_stop_action",
"allow_multiple_items",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
@@ -89,6 +91,23 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "Stop",
+ "depends_on": "maintain_same_rate",
+ "description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
+ "fieldname": "maintain_same_rate_action",
+ "fieldtype": "Select",
+ "label": "Action If Same Rate is Not Maintained",
+ "mandatory_depends_on": "maintain_same_rate",
+ "options": "Stop\nWarn"
+ },
+ {
+ "depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
+ "fieldname": "role_to_override_stop_action",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Override Stop Action",
+ "options": "Role"
}
],
"icon": "fa fa-cog",
@@ -96,7 +115,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-03-02 17:34:04.190677",
+ "modified": "2021-04-04 20:01:44.087066",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index d32e98e..ef9372e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -133,6 +133,7 @@
d.material_request_item, "schedule_date")
+ @frappe.whitelist()
def get_last_purchase_rate(self):
"""get last purchase rates for all items"""
@@ -252,6 +253,7 @@
self.update_prevdoc_status()
# Must be called after updating ordered qty in Material Request
+ # bin uses Material Request Items to recalculate & update
self.update_requested_qty()
self.update_ordered_qty()
@@ -366,7 +368,6 @@
"Purchase Order": {
"doctype": "Purchase Receipt",
"field_map": {
- "per_billed": "per_billed",
"supplier_warehouse":"supplier_warehouse"
},
"validation": {
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 604c886..3c4f908 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -90,6 +90,50 @@
frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0)
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
+ def test_update_remove_child_linked_to_mr(self):
+ """Test impact on linked PO and MR on deleting/updating row."""
+ mr = make_material_request(qty=10)
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.save()
+ po.submit()
+
+ first_item_of_po = po.get("items")[0]
+ existing_ordered_qty = get_ordered_qty() # 10
+ existing_requested_qty = get_requested_qty() # 0
+
+ # decrease ordered qty by 3 (10 -> 7) and add item
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': 7,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+ mr.reload()
+
+ # requested qty increases as ordered qty decreases
+ self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3
+ self.assertEqual(mr.items[0].ordered_qty, 7)
+
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7
+
+ # delete first item linked to Material Request
+ trans_item = json.dumps([
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+ mr.reload()
+
+ # requested qty increases as ordered qty is 0 (deleted row)
+ self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10
+ self.assertEqual(mr.items[0].ordered_qty, 0)
+
+ # ordered qty decreases as ordered qty is 0 (deleted row)
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
def test_update_child(self):
mr = make_material_request(qty=10)
@@ -120,7 +164,6 @@
self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
-
def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
@@ -129,6 +172,7 @@
pr = make_pr_against_po(po.name, 2)
po.load_from_db()
+ existing_ordered_qty = get_ordered_qty()
first_item_of_po = po.get("items")[0]
trans_item = json.dumps([
@@ -145,7 +189,8 @@
po.reload()
self.assertEquals(len(po.get('items')), 2)
self.assertEqual(po.status, 'To Receive and Bill')
-
+ # ordered qty should increase on row addition
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
def test_update_child_removing_item(self):
po = create_purchase_order(do_not_save=1)
@@ -156,6 +201,7 @@
po.reload()
first_item_of_po = po.get("items")[0]
+ existing_ordered_qty = get_ordered_qty()
# add an item
trans_item = json.dumps([
{
@@ -168,6 +214,10 @@
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
+
+ # ordered qty should increase on row addition
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
+
# check if can remove received item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
@@ -187,6 +237,9 @@
self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill')
+ # ordered qty should decrease (back to initial) on row deletion
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty)
+
def test_update_child_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4)
@@ -230,11 +283,13 @@
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
- new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template - _TC",
- "valid_from": nowdate()
- })
- new_item_with_tax.save()
+ if not frappe.db.exists("Item Tax",
+ {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}):
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template - _TC",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 7cf22f8..b530d1a 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -66,6 +66,7 @@
def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled')
+ @frappe.whitelist()
def get_supplier_email_preview(self, supplier):
"""Returns formatted email preview as string."""
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))
diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 6e6eaed..2528240 100644
--- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
@@ -9,9 +9,7 @@
class TestSupplierScorecard(unittest.TestCase):
def test_create_scorecard(self):
- delete_test_scorecards()
- my_doc = make_supplier_scorecard()
- doc = my_doc.insert()
+ doc = make_supplier_scorecard().insert()
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self):
@@ -121,7 +119,8 @@
{
"weight":100.0,
"doctype":"Supplier Scorecard Scoring Criteria",
- "criteria_name":"Delivery"
+ "criteria_name":"Delivery",
+ "formula": "100"
}
],
"supplier":"_Test Supplier",
diff --git a/erpnext/change_log/v13/v13_0_0.md b/erpnext/change_log/v13/v13_0_0.md
new file mode 100644
index 0000000..a6cebab
--- /dev/null
+++ b/erpnext/change_log/v13/v13_0_0.md
@@ -0,0 +1,471 @@
+# Version 13.0.0 Release Notes
+
+### Accounting
+- [New and refreshed POS](https://github.com/frappe/erpnext/pull/20789)
+- [GST E-invoicing for India](https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing)
+- [Distributed Cost Center](https://docs.erpnext.com/docs/user/manual/en/accounts/distributed-cost-center)
+- [Process Bulk Statement Of Accounts](https://docs.erpnext.com/docs/user/manual/en/accounts/process-statement-of-accounts)
+- [More controlled deferred revenue booking](https://docs.erpnext.com/docs/user/manual/en/accounts/process-deferred-accounting)
+- [Dunning](https://docs.erpnext.com/docs/user/manual/en/accounts/dunning)
+- [Journal Entry Template](https://docs.erpnext.com/docs/user/manual/en/accounts/journal-entry-template)
+- [POS Register report](https://github.com/frappe/erpnext/pull/23313)
+- [UAE VAT 201 Report](https://github.com/frappe/erpnext/pull/23447)
+
+
+### Loan Management
+- [Loan Application](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-application)
+- [Loan](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan)
+- [Loan Security Pledge](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-security-pledge)
+- [Loan Disbursement](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-disbursement)
+- [Loan Repayment](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-repayment)
+- [Loan Interest Accrual](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-interest-accrual)
+- [Loan Write Off](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-write-off)
+
+### Healthcare
+- [Refactored Healthcare Module](https://docs.erpnext.com/docs/user/manual/en/healthcare)
+- [Rehabilitation Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/exercise_type)
+- [Laboratory Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/setup_laboratory)
+- [Patient Progress Page](https://github.com/frappe/erpnext/pull/22474)
+- [Inpatient Medication Order and Entry](https://docs.erpnext.com/docs/user/manual/en/healthcare/inpatient_medication_entry)
+- [Therapy Plan Template](https://docs.erpnext.com/docs/user/manual/en/healthcare/therapy_plan)
+- [Multi company support in Healthcare](https://github.com/frappe/erpnext/pull/21290)
+- [Inpatient Medication Orders Script Report](https://github.com/frappe/erpnext/pull/23984)
+- [Patient History Enhancements](https://github.com/frappe/erpnext/pull/24033)
+
+
+### Stock
+- [Putaway](https://docs.erpnext.com/docs/user/manual/en/stock/putaway-rule)
+- [More accurate stock valuation in case of back-dated stock transactions](https://github.com/frappe/erpnext/pull/24183)
+- [Repost item costing via background job](https://github.com/frappe/erpnext/pull/24183)
+- [Item valuation for internal stock transfers](https://github.com/frappe/erpnext/pull/24200)
+- [Multi currency in Landed Cost Voucher](https://github.com/frappe/erpnext/pull/24127)
+- [Formula based Quality Inspection](https://docs.erpnext.com/docs/user/manual/en/stock/quality-inspection)
+- [Value Based and Numeric Quality Inspection](https://github.com/frappe/erpnext/pull/24181)
+- [Shipment](https://github.com/frappe/erpnext/pull/22914)
+- [Return tracking in PR/DN](https://github.com/frappe/erpnext/pull/22859)
+
+### Manufacturing
+- [Production forecasting using Exponential Smoothing method](https://docs.erpnext.com/docs/user/manual/en/manufacturing/reports/demand-driven-forecasting)
+- [BOM Template](https://docs.erpnext.com/docs/user/manual/en/manufacturing/bill-of-materials#34-bom-template)
+- [Downtime Entry](https://docs.erpnext.com/docs/user/manual/en/manufacturing/downtime-entry)
+- [Quality Inspection on Job Card](https://github.com/frappe/erpnext/pull/23964)
+- New Reports
+ - Production Planning Report ([#21763](https://github.com/frappe/erpnext/pull/21763))
+ - BOM Operations Time ([#21763](https://github.com/frappe/erpnext/pull/21763))
+ - Work Order Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
+ - Job card Summary ([#21430](https://github.com/frappe/erpnext/pull/21430))
+ - Downtime Analysis ([#21430](https://github.com/frappe/erpnext/pull/21430))
+ - Quality Inspection ([#21430](https://github.com/frappe/erpnext/pull/21430))
+
+### HR
+- [Leave policy assignment](https://github.com/frappe/erpnext/pull/23112)
+- [In and Out time in attendance](https://github.com/frappe/erpnext/pull/21547)
+- [Shift management](https://docs.erpnext.com/docs/user/manual/en/human-resources/shift-management)
+- [Recruitment analytics](https://github.com/frappe/erpnext/pull/21732)
+- [Bulk Mark Attendance](https://github.com/frappe/erpnext/pull/20062)
+- [Leave type with partial payment](https://github.com/frappe/erpnext/pull/23173)
+- New and enhanced reports
+ - Employee Analytics ([#21705](https://github.com/frappe/erpnext/pull/21705))
+ - Employee Leave Balance ([#20754](https://github.com/frappe/erpnext/pull/20754))
+ - Employee Leave Balance Summary ([#20754](https://github.com/frappe/erpnext/pull/20754))
+
+### Payroll
+- [Multi-currency payroll](https://github.com/frappe/erpnext/pull/23519)
+- [Payroll based on attendance](https://github.com/frappe/erpnext/pull/21258)
+- [Payroll based on employee cost center](https://github.com/frappe/erpnext/pull/21609)
+- [Recurring Additional Salary](https://github.com/frappe/erpnext/pull/20936)
+- [Compute Year to Date for Salary Slip components](https://github.com/frappe/erpnext/pull/24362)
+- New Reports
+ - Income Tax Deductions
+ - Professional Tax Deductions
+ - Provident Fund Deductions
+ - Total Salary Payments Based on Payment Mode
+ - Salary Payments via ECS
+
+### CRM
+- [Social Media Post](https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post)
+- [Make Quotation against Blanket Order](https://docs.erpnext.com/docs/user/manual/en/selling/blanket-order)
+- [Calendar View for Opportunity](https://github.com/frappe/erpnext/pull/21280)
+
+### Selling
+- [Batch wise item pricing](https://github.com/frappe/erpnext/pull/24470)
+- [Refreshed shopping cart](https://github.com/frappe/erpnext/pull/22617)
+- [Territory-wise Sales Report](https://github.com/frappe/erpnext/pull/20428)
+
+#### Buying
+- [Multi UOM support in Request for Quotation](https://github.com/frappe/erpnext/pull/22249)
+- [Provision to make RFQ against Opportunity](https://github.com/frappe/erpnext/pull/22765)
+- [Item Rate in Stock UOM in purchase cycle](https://github.com/frappe/erpnext/pull/24315)
+- New Reports
+ - Requested Items To Order ([#21611](https://github.com/frappe/erpnext/pull/21611))
+ - Purchase Order Analysis ([#21611](https://github.com/frappe/erpnext/pull/21611))
+ - Supplier Quotation Comparison report ([#23323](https://github.com/frappe/erpnext/pull/23323))
+
+### Project
+- [Project template with dependent tasks](https://github.com/frappe/erpnext/pull/24092)
+- [Project Summary Report](https://github.com/frappe/erpnext/pull/21587)
+
+### Support
+- [Help Articles on support portal](https://github.com/frappe/erpnext/pull/22194)
+- [Issue Metrics and SLA Enhancements](https://github.com/frappe/erpnext/pull/21617)
+- [Issue Summary Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
+- [Issue Analytics Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports)
+
+### Non-Profits
+- [80G Certificates and Donations](https://docs.erpnext.com/docs/user/manual/en/non_profit/tax_exemption_80g_certificate)
+
+#### Integrations
+- [Woocommerce Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/woocommerce_integration)
+- [Taxjar Integration](https://github.com/frappe/erpnext/pull/21047)
+- [M-pesa Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/mpesa-integration)
+- [Telephony feature using Twillio](https://github.com/frappe/erpnext/pull/24032)
+- [Voice Call Settings](https://github.com/frappe/erpnext/pull/24126)
+
+
+#### Other Enhancements and Fixes
+- Accounting Dimensions in Budget Variance Report ([#19973](https://github.com/frappe/erpnext/pull/19973))
+- "Sync Now" option in Plaid Settings ([#23602](https://github.com/frappe/erpnext/pull/23602))
+- Custom Fields in POS ([#19876](https://github.com/frappe/erpnext/pull/19876))
+- [Inter Warehouse Stock Transfer in Purchase Receipt](https://docs.erpnext.com/docs/user/manual/en/stock/articles/material-transfer-from-delivery-note)
+- [Accounts Payable Report based on Payment Terms](https://docs.erpnext.com/docs/user/manual/en/accounts/accounting-reports)
+- Configurable accounting dimension filters and validations ([#23912](https://github.com/frappe/erpnext/pull/23912))
+- Territory tree in Customer Acquisition and Loyalty report ([#21668](https://github.com/frappe/erpnext/pull/21668))
+- Allow Purchase Invoice Creation Without Purchase Order Checkbox in Supplier ([#20864](https://github.com/frappe/erpnext/pull/20864))
+- Gross Profit In Quotation ([#21795](https://github.com/frappe/erpnext/pull/21795))
+- Notify credit controller users for credit limit extension via Email ([#22213](https://github.com/frappe/erpnext/pull/22213))
+- Run MRP at parent level in the production plan and make material transfer based upon materials availability ([#21545](https://github.com/frappe/erpnext/pull/21545))
+- Balance Serial Nos in Stock Ledger report ([#23675](https://github.com/frappe/erpnext/pull/23675))
+- Youtube interactions via Video ([#22867](https://github.com/frappe/erpnext/pull/22867))
+- Consider Holiday List in Student Leave Application and Attendance ([#23388](https://github.com/frappe/erpnext/pull/23388))
+- Patient appointment status changes ([#24201](https://github.com/frappe/erpnext/pull/24201))
+- Sales order status filter added for production plan ([#23805](https://github.com/frappe/erpnext/pull/23805))
+- Monthly attendance sheet report group by Department, Designation, Employee Grade and Branch ([#21331](https://github.com/frappe/erpnext/pull/21331))
+- Upload Attendance template now have pre-filled holiday status ([#20947](https://github.com/frappe/erpnext/pull/20947))
+- Provision to disable serial no and batch selector ([#24398](https://github.com/frappe/erpnext/pull/24398))
+
+<details>
+<summary>More</summary>
+
+- Fetch Items from BOM in Stock Entry([#19498](https://github.com/frappe/erpnext/pull/19498))
+- Supplier Sourced Items in BOM ([#23557](https://github.com/frappe/erpnext/pull/23557))
+- Close Production Plan ([#23728](https://github.com/frappe/erpnext/pull/23728))
+- Button to create Stock Entry for Drug Shortage ([#24012](https://github.com/frappe/erpnext/pull/24012))
+- Added column cost center in Accounts Receivable report ([#23835](https://github.com/frappe/erpnext/pull/23835))
+- Added jinja templating in Contract Template ([#24046](https://github.com/frappe/erpnext/pull/24046))
+- Make account number length configurable ([#23845](https://github.com/frappe/erpnext/pull/23845))
+- Add company and correct filter in bank reconciliation statement ([#23614](https://github.com/frappe/erpnext/pull/23614))
+- Added Condition field in Pricing Rule ([#23014](https://github.com/frappe/erpnext/pull/23014))
+- Open lead status on next contact date ([#23445](https://github.com/frappe/erpnext/pull/23445))
+- [Tax Category in POS Profile](https://docs.erpnext.com/docs/user/manual/en/accounts/pos-profile)
+- Added phone field in product Inquiry ([#23170](https://github.com/frappe/erpnext/pull/23170))
+- Allow Discharge despite Unbilled Healthcare Services ([#24281](https://github.com/frappe/erpnext/pull/24281))
+- Do Not Bill Patient Encounters for Inpatients ([#24355](https://github.com/frappe/erpnext/pull/24355))
+- Autofill Supplier pop-up when only 1 Supplier in RFQ ([#22512](https://github.com/frappe/erpnext/pull/22512))
+- Accounting entries for service item in Purchase receipt ([#22223](https://github.com/frappe/erpnext/pull/22223))
+- Added Project in Sales Analytics report ([#23309](https://github.com/frappe/erpnext/pull/23309))
+- Added all companies option in employee tree to view employee across all companies ([#22573](https://github.com/frappe/erpnext/pull/22573))
+- Email Group Option In Email Campaign ([#22731](https://github.com/frappe/erpnext/pull/22731))
+- Stock Report Enhancements ([#21727](https://github.com/frappe/erpnext/pull/21727))
+- Added range for age in stock ageing ([#22622](https://github.com/frappe/erpnext/pull/22622))
+- Report Summary in Financial Statement([#20876](https://github.com/frappe/erpnext/pull/20876))
+- Added sequence id in routing for the completion of operations sequentially ([#23641](https://github.com/frappe/erpnext/pull/23641))
+- Nested Set filtering for Accounting Dimension
+- Add/Remove Items from submitted Sales/Purchase Order
+- Provision to edit Item Details from Marketplace
+- Scan Barcode in Purchase Receipt
+- Disable Rounded Totals Checkbox for Salary Slips in HR Settings
+
+- Renamed Loan Management to Loan on Desk Page ([#21877](https://github.com/frappe/erpnext/pull/21877))
+- Added Expense Approver field in Employee master ([#22244](https://github.com/frappe/erpnext/pull/22244))
+- Bill all hours by default on Timesheet ([#22155](https://github.com/frappe/erpnext/pull/22155))
+- Unable to cancel employee advance ([#22374](https://github.com/frappe/erpnext/pull/22374))
+- Status error in purchase invoice ([#22351](https://github.com/frappe/erpnext/pull/22351))
+- Item-wise sales and purchase register export ([#22184](https://github.com/frappe/erpnext/pull/22184))
+- Billing address in for Purchase documents ([#22233](https://github.com/frappe/erpnext/pull/22233))
+- Handle canceled entries in financial statements ([#22231](https://github.com/frappe/erpnext/pull/22231))
+- Default period start date and period end date for financial statements ([#22011](https://github.com/frappe/erpnext/pull/22011))
+- Update Packed Items via Update Items in Sales Order ([#22392](https://github.com/frappe/erpnext/pull/22392))
+- Hide delete company transactions button if not system manager ([#21839](https://github.com/frappe/erpnext/pull/21839))
+- Skipping total row for tree-view reports ([#22350](https://github.com/frappe/erpnext/pull/22350))
+- Cancelled entries in tds payable monthly report ([#22131](https://github.com/frappe/erpnext/pull/22131))
+- Inter-company Invoice currency for multicurrency transactions ([#21984](https://github.com/frappe/erpnext/pull/21984))
+- Filter batches based on item and warehouse in Pick List (develop) ([#21780](https://github.com/frappe/erpnext/pull/21780))
+- Set cost center in Expense Claim child based on parent (if missing) ([#22175](https://github.com/frappe/erpnext/pull/22175))
+- Item wise backdated stock entry posting for immutable ledger ([#22366](https://github.com/frappe/erpnext/pull/22366))
+- Shopping cart UI fixes ([#22137](https://github.com/frappe/erpnext/pull/22137))
+- Filter Leave Type based on allocation for a particular employee ([#22050](https://github.com/frappe/erpnext/pull/22050))
+- Party validation for inter-warehouse transaction ([#22186](https://github.com/frappe/erpnext/pull/22186))
+- Manufacturing dashboard and work order summary chart ([#21946](https://github.com/frappe/erpnext/pull/21946))
+- IP Admission and Discharge, Minor fixes ([#21817](https://github.com/frappe/erpnext/pull/21817))
+- Validation of Purchase Order against Material Request missing ([#22192](https://github.com/frappe/erpnext/pull/22192))
+- Staffing Plan validation ([#22379](https://github.com/frappe/erpnext/pull/22379))
+- Do not allow backdated stock transactions in previous fiscal year ([#21967](https://github.com/frappe/erpnext/pull/21967))
+- Employee Advance Return not working ([#21812](https://github.com/frappe/erpnext/pull/21812))
+- Added card for reports on education desk ([#21853](https://github.com/frappe/erpnext/pull/21853))
+- Refactored project summary report ([#21943](https://github.com/frappe/erpnext/pull/21943))
+- Revenue and Customer Count only in date range in Customer Acquitition Report ([#22210](https://github.com/frappe/erpnext/pull/22210))
+- Alternative item not working for subcontract ([#22386](https://github.com/frappe/erpnext/pull/22386))
+- Unable to create batched Item ([#22393](https://github.com/frappe/erpnext/pull/22393))
+- Filters for the manufacturing reports ([#21960](https://github.com/frappe/erpnext/pull/21960))
+- Raw material warehouse in Production Planning Report ([#21982](https://github.com/frappe/erpnext/pull/21982))
+- Allowed LWP leave types to select in Leave Application even if there is no allocation against them ([#22197](https://github.com/frappe/erpnext/pull/22197))
+- Report not working on parameter Grade ([#21951](https://github.com/frappe/erpnext/pull/21951))
+- Allow to enter Relieving date if employee status is Left ([#22242](https://github.com/frappe/erpnext/pull/22242))
+- Resetting lost reason in opportunity and quotation ([#22378](https://github.com/frappe/erpnext/pull/22378))
+- Filtering issues in opening invoice creation tool ([#21969](https://github.com/frappe/erpnext/pull/21969))
+- Set default reference Id for "On Previous Row Amount" and "On Previous Row Total" ([#22346](https://github.com/frappe/erpnext/pull/22346))
+- UX date range field separated in from and to date fields. ([#21765](https://github.com/frappe/erpnext/pull/21765))
+- Enable show_configure_button when shopping cart is enabled ([#22468](https://github.com/frappe/erpnext/pull/22468))
+- Setup status indicators for Job Offer and Job Applicant (develop) ([#22445](https://github.com/frappe/erpnext/pull/22445))
+- Item-wise sales history report ([#22783](https://github.com/frappe/erpnext/pull/22783))
+- Setting filter for project in kanban board ([#22717](https://github.com/frappe/erpnext/pull/22717))
+- Dashboard For Timesheet ([#22750](https://github.com/frappe/erpnext/pull/22750))
+- Handle custom statuses for the pause SLA configuration ([#22349](https://github.com/frappe/erpnext/pull/22349))
+- Quality Feedback and Template ([#22571](https://github.com/frappe/erpnext/pull/22571))
+- Unable to change link from new lead to existing customer ([#22787](https://github.com/frappe/erpnext/pull/22787))
+- Move Issue List actions under 'Actions' dropdown (ux) ([#22710](https://github.com/frappe/erpnext/pull/22710))
+- Cost center should only show option of selected company ([#22598](https://github.com/frappe/erpnext/pull/22598))
+- Serial No Rename does not affect Stock Ledger Entry ([#22746](https://github.com/frappe/erpnext/pull/22746))
+- Descriptions not copied while creating Fees from Fee Structure ([#22792](https://github.com/frappe/erpnext/pull/22792))
+- Company filter for cost_center and expense_account in all sales and purchase transactions ([#22478](https://github.com/frappe/erpnext/pull/22478))
+- Arrangements of filters for reports accounts payable & receivable ([#22636](https://github.com/frappe/erpnext/pull/22636))
+- Update the project after task deletion so that the % completed shows correct value ([#22591](https://github.com/frappe/erpnext/pull/22591))
+- Block Invalid Serial No updates in Maintenance Schedule ([#22665](https://github.com/frappe/erpnext/pull/22665))
+- Fetch item price in sales invoice based on it's validity ([#22563](https://github.com/frappe/erpnext/pull/22563))
+- Add view ledger button for cancelled docs ([#22432](https://github.com/frappe/erpnext/pull/22432))
+- Allow creating SLA documents even if SLA tracking is not enabled ([#22608](https://github.com/frappe/erpnext/pull/22608))
+- Quotation list view blank if quotation_to field not set as a standard filter ([#22672](https://github.com/frappe/erpnext/pull/22672))
+- Salary deductions report fixes ([#22397](https://github.com/frappe/erpnext/pull/22397))
+22727))
+- Incorrect delivered qty in Supplier-Wise Sales Analytics ([#22631](https://github.com/frappe/erpnext/pull/22631))
+- Moved parent warehouse to top section also added a section break ([#22708](https://github.com/frappe/erpnext/pull/22708))
+- Skip Progress and Completed by fields on Task Duplication ([#22565](https://github.com/frappe/erpnext/pull/22565))
+- Incorrect stock after merging the items ([#22526](https://github.com/frappe/erpnext/pull/22526))
+- Letter head not found in opening invoice creation tool ([#22488](https://github.com/frappe/erpnext/pull/22488))
+- Cannot cancel asset and asset movement ([#22441](https://github.com/frappe/erpnext/pull/22441))
+- Fetch project-related info in Timesheet ([#22423](https://github.com/frappe/erpnext/pull/22423))
+- Currency symbol not showing as per company currency in stock balance report ([#22724](https://github.com/frappe/erpnext/pull/22724))
+- Add default cost center in payment reconciliation JV ([#22614](https://github.com/frappe/erpnext/pull/22614))
+- Stock Reconciliation Invalid Quantity for Batched Item ([#22726](https://github.com/frappe/erpnext/pull/22726))
+- Project link not set in accounts other than profit and loss accounts ([#22051](https://github.com/frappe/erpnext/pull/22051))
+- Buying price for non stock item in gross profit report ([#22616](https://github.com/frappe/erpnext/pull/22616))
+- Multi currency payment reconciliation ([#22738](https://github.com/frappe/erpnext/pull/22738))
+- Cannot cancel assets with repair pending ([#22440](https://github.com/frappe/erpnext/pull/22440))
+- Reset homepage to home after unchecking products page ([#22736](https://github.com/frappe/erpnext/pull/22736))
+- Generic Message in previous doc validation for buying and selling ([#22546](https://github.com/frappe/erpnext/pull/22546))
+- Expense claim outstanding while making payment entry ([#22735](https://github.com/frappe/erpnext/pull/22735))
+- Take parent cost center for child if no cost center at child in expense claim ([#22496](https://github.com/frappe/erpnext/pull/22496))
+- Consider company fiscal year for getting balance ([#22577](https://github.com/frappe/erpnext/pull/22577))
+- Pick List empty table and Serial-Batch items handling ([#22426](https://github.com/frappe/erpnext/pull/22426))
+- Show total row in print format of financial statement ([#22693](https://github.com/frappe/erpnext/pull/22693))
+- Set Root as Parent if no parent in new tree view node ([#22497](https://github.com/frappe/erpnext/pull/22497))
+- Multiple pos issues ([#23725](https://github.com/frappe/erpnext/pull/23725))
+- Calculate taxes if tax is based on item quantity and inclusive on item price ([#23001](https://github.com/frappe/erpnext/pull/23001))
+- Contact us button not visible in the website for the non variant items ([#23217](https://github.com/frappe/erpnext/pull/23217))
+- Not able to make Material Request from Sales Order ([#23669](https://github.com/frappe/erpnext/pull/23669))
+- Capture advance payments in payment order ([#23256](https://github.com/frappe/erpnext/pull/23256))
+- Program and Course Enrollment fixes ([#23333](https://github.com/frappe/erpnext/pull/23333))
+- Cannot create asset if cwip disabled and account not set ([#23580](https://github.com/frappe/erpnext/pull/23580))
+- Cannot merge pos invoices with inclusive tax ([#23541](https://github.com/frappe/erpnext/pull/23541))
+- Do not allow Company as accounting dimension ([#23755](https://github.com/frappe/erpnext/pull/23755))
+- Set value of wrong Bank Account field in Payment Entry ([#22302](https://github.com/frappe/erpnext/pull/22302))
+- Reverse journal entry for multi-currency ([#23165](https://github.com/frappe/erpnext/pull/23165))
+- Updated integrations desk page ([#23772](https://github.com/frappe/erpnext/pull/23772))
+- Assessment Result child table not visible when accessed via Assessment Plan dashboard ([#22880](https://github.com/frappe/erpnext/pull/22880))
+- Conversion factor fixes in Stock Entry ([#23407](https://github.com/frappe/erpnext/pull/23407))
+- Total calculations for multi-currency RCM invoices ([#23072](https://github.com/frappe/erpnext/pull/23072))
+- Show accounts in financial statements upto level 20 ([#23718](https://github.com/frappe/erpnext/pull/23718))
+- Consolidated financial statement sums values into wrong parent ([#23288](https://github.com/frappe/erpnext/pull/23288))
+- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
+- Added missing reports on selling desk ([#23548](https://github.com/frappe/erpnext/pull/23548))
+- Fixed heading in the mobile view ([#23145](https://github.com/frappe/erpnext/pull/23145))
+- Misleading filters on Item tax Template Link field ([#22918](https://github.com/frappe/erpnext/pull/22918))
+- Do not consider opening entries for TDS calculation ([#23597](https://github.com/frappe/erpnext/pull/23597))
+- Attendance calendar map fix ([#23245](https://github.com/frappe/erpnext/pull/23245))
+- Post cancellation accounting entry on posting date instead of current ([#23361](https://github.com/frappe/erpnext/pull/23361))
+- Set Customer only if Contact is present ([#23704](https://github.com/frappe/erpnext/pull/23704))
+- Add Delivery Note Count in Sales Invoice Dashboard ([#23161](https://github.com/frappe/erpnext/pull/23161))
+- Breadcrumbs for Maintenance Visit and Schedule ([#23369](https://github.com/frappe/erpnext/pull/23369))
+- Raise Error on over receipt/consumption for sub-contracted PR ([#23195](https://github.com/frappe/erpnext/pull/23195))
+- Validate if company not set in the Payment Entry ([#23419](https://github.com/frappe/erpnext/pull/23419))
+- Ignore company and bank account doctype while deleting company transactions ([#22953](https://github.com/frappe/erpnext/pull/22953))
+- Sales funnel data is inconsistent ([#23110](https://github.com/frappe/erpnext/pull/23110))
+- Credit Limit Email not working ([#23059](https://github.com/frappe/erpnext/pull/23059))
+- Add Company in list fields to fetch for Expense Claim ([#23007](https://github.com/frappe/erpnext/pull/23007))
+- Issue form cleaned up and renamed Minutes to First Response field ([#23066](https://github.com/frappe/erpnext/pull/23066))
+- Quotation lost reason options fix ([#22814](https://github.com/frappe/erpnext/pull/22814))
+- Tax amounts in HSN Wise Outward summary ([#23076](https://github.com/frappe/erpnext/pull/23076))
+- Patient Appointment not able to save ([#23434](https://github.com/frappe/erpnext/pull/23434))
+- Removed Working Hours field from Company ([#23009](https://github.com/frappe/erpnext/pull/23009))
+- Added check-in time validation in the Inpatient Record - Transfer ([#22958](https://github.com/frappe/erpnext/pull/22958))
+- Handle Blank from/to range in Numeric Item Attribute ([#23483](https://github.com/frappe/erpnext/pull/23483))
+- Sequence Matcher error in Bank Reconciliation ([#23539](https://github.com/frappe/erpnext/pull/23539))
+- Fixed Conversion Factor rate for the BOM Exploded Item ([#23151](https://github.com/frappe/erpnext/pull/23151))
+- Payment Schedule not fetching ([#23476](https://github.com/frappe/erpnext/pull/23476))
+- Validate if removed Item Attributes exist in variant items ([#22911](https://github.com/frappe/erpnext/pull/22911))
+- Set default billing address for purchase documents ([#22950](https://github.com/frappe/erpnext/pull/22950))
+- Added help link in navbar settings ([#22943](https://github.com/frappe/erpnext/pull/22943))
+- Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt ([#23282](https://github.com/frappe/erpnext/pull/23282))
+- Education Module fixes ([#23714](https://github.com/frappe/erpnext/pull/23714))
+- Filter out cancelled entries in customer ledger summary ([#23205](https://github.com/frappe/erpnext/pull/23205))
+- Fiscal Year and Tax Rates for Italy ([#23623](https://github.com/frappe/erpnext/pull/23623))
+- Production Plan incorrect Work Order qty ([#23264](https://github.com/frappe/erpnext/pull/23264))
+- Added new filters in the Batch-wise Balance History report ([#23676](https://github.com/frappe/erpnext/pull/23676))
+- Update state code and union territory for Daman and Diu ([#22988](https://github.com/frappe/erpnext/pull/22988))
+- Set Stock UOM in item while creating Material Request from Stock Entry ([#23436](https://github.com/frappe/erpnext/pull/23436))
+- Sales Order to Purchase Order flow improvement ([#23357](https://github.com/frappe/erpnext/pull/23357))
+- Student Admission and Student Applicant fixes ([#23515](https://github.com/frappe/erpnext/pull/23515))
+- Loan disbursement amount validation ([#24000](https://github.com/frappe/erpnext/pull/24000))
+- Making company address read-only in delivery note ([#23890](https://github.com/frappe/erpnext/pull/23890))
+- BOM stock report color showing always red ([#23994](https://github.com/frappe/erpnext/pull/23994))
+- Added filter for customer field in Issue ([#24051](https://github.com/frappe/erpnext/pull/24051))
+- Added project link in timesheet form ([#23764](https://github.com/frappe/erpnext/pull/23764))
+- Update integrations desk page ([#23767](https://github.com/frappe/erpnext/pull/23767))
+- Place of supply change on address change ([#23941](https://github.com/frappe/erpnext/pull/23941))
+- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23672](https://github.com/frappe/erpnext/pull/23672))
+- Auto fetch serial nos with modified conversion factor ([#23854](https://github.com/frappe/erpnext/pull/23854))
+- Default cost center in item master not set in stock entry ([#23877](https://github.com/frappe/erpnext/pull/23877))
+- Incorrect de-link serial no and batch ([#23947](https://github.com/frappe/erpnext/pull/23947))
+- Accounting for internal transfer invoices within same company ([#24021](https://github.com/frappe/erpnext/pull/24021))
+- Multiple pricing rule with margin type as Percentage is not working ([#24205](https://github.com/frappe/erpnext/pull/24205))
+- Added Purchase Order to Global Search ([#24055](https://github.com/frappe/erpnext/pull/24055))
+- Cannot expand row in update items dialog ([#23839](https://github.com/frappe/erpnext/pull/23839))
+- Maintain stock can't be changed it there is product bundle ([#23989](https://github.com/frappe/erpnext/pull/23989))
+- SO to PO Mapping Issue ([#23820](https://github.com/frappe/erpnext/pull/23820))
+- Asset with value zero doesn't show up in fixed asset register ([#24091](https://github.com/frappe/erpnext/pull/24091))
+- Cannot save customer email & phone ([#23797](https://github.com/frappe/erpnext/pull/23797))
+- Incorrect balance value in stock balance report ([#24048](https://github.com/frappe/erpnext/pull/24048))
+- Payment Terms not fetched in Purchase Invoice from Purchase Receipt ([#23735](https://github.com/frappe/erpnext/pull/23735))
+- Fix for LMS Sign Up link ([#23743](https://github.com/frappe/erpnext/pull/23743))
+- Incorrect stock quantity if 'Allow Multiple Material Consumption… ([#24116](https://github.com/frappe/erpnext/pull/24116))
+- Added wrong absent days calculation in salary slip ([#23897](https://github.com/frappe/erpnext/pull/23897))
+- Purchase receipt to purchase invoice bill date mapping ([#23967](https://github.com/frappe/erpnext/pull/23967))
+- Overriding po ([#24022](https://github.com/frappe/erpnext/pull/24022))
+- Do not cancel reference document on Quality Inspection cancellation ([#24198](https://github.com/frappe/erpnext/pull/24198))
+- Get formatted value in 'taxes' print template ([#24035](https://github.com/frappe/erpnext/pull/24035))
+- Don't overrule Item Price via Pricing Rule Rate if 0 ([#23636](https://github.com/frappe/erpnext/pull/23636))
+- Job card error handling for operations field ([#23991](https://github.com/frappe/erpnext/pull/23991))
+- Validation for journal entry with 0 debit and credit values ([#23975](https://github.com/frappe/erpnext/pull/23975))
+- Check if customer exists in product listing ([#24030](https://github.com/frappe/erpnext/pull/24030))
+- Asset finance book posting date fix ([#23778](https://github.com/frappe/erpnext/pull/23778))
+- Same source and target tables in Status Updater's update query ([#24110](https://github.com/frappe/erpnext/pull/24110))
+- Asset finance book depreciation posting date fix ([#23833](https://github.com/frappe/erpnext/pull/23833))
+- Ignore exception during leave ledger creation from patch ([#24005](https://github.com/frappe/erpnext/pull/24005))
+- Added link of bank reconciliation and clearance in accounting desk page ([#23850](https://github.com/frappe/erpnext/pull/23850))
+- Sales invoice add button from sales order dashboard ([#24077](https://github.com/frappe/erpnext/pull/24077))
+- Incorrect calculation for consumed qty for subcontract item ([#23257](https://github.com/frappe/erpnext/pull/23257))
+- Incorrect required_qty in Production Planning Report ([#24074](https://github.com/frappe/erpnext/pull/24074))
+- Email digest user not found ([#23949](https://github.com/frappe/erpnext/pull/23949))
+- Delete Receive at Warehouse entry on cancellation of Send to War… ([#24115](https://github.com/frappe/erpnext/pull/24115))
+- Added TDS Payable account number and an error message ([#24065](https://github.com/frappe/erpnext/pull/24065))
+- Override field_map for job card gantt ([#24155](https://github.com/frappe/erpnext/pull/24155))
+- Old shopify order syncing date ([#23990](https://github.com/frappe/erpnext/pull/23990))
+- Shipping chanrges not sync in erpnext from shopify ([#24114](https://github.com/frappe/erpnext/pull/24114))
+- GSTR B2C report ([#24039](https://github.com/frappe/erpnext/pull/24039))
+- Ignore cancelled entries in stock balance report ([#23757](https://github.com/frappe/erpnext/pull/23757))
+- Stock ageing report not working ([#23923](https://github.com/frappe/erpnext/pull/23923))
+- Incorrect assign to in Maintenance Schedule ([#23831](https://github.com/frappe/erpnext/pull/23831))
+- Improve UX of DATEV report ([#23892](https://github.com/frappe/erpnext/pull/23892))
+- Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765))
+- dDouble exception in payroll ([#24078](https://github.com/frappe/erpnext/pull/24078))
+- Make asset dashboard charts public ([#23751](https://github.com/frappe/erpnext/pull/23751))
+- Don't copy terms and discount from SO to PO ([#23903](https://github.com/frappe/erpnext/pull/23903))
+- Ignore doctypes on company transaction delete ([#23864](https://github.com/frappe/erpnext/pull/23864))
+- Error handling in Upload Attendance ([#23907](https://github.com/frappe/erpnext/pull/23907))
+- Tax template update on customer address change ([#24160](https://github.com/frappe/erpnext/pull/24160))
+- Not able to save bom ([#23910](https://github.com/frappe/erpnext/pull/23910))
+- Enable Allow Auto Repeat for standard doctypes having auto_repeat field ([#23776](https://github.com/frappe/erpnext/pull/23776))
+- Place of Supply fix in Sales Invoices ([#23785](https://github.com/frappe/erpnext/pull/23785))
+- Opening invoices in GSTR-1 report ([#24117](https://github.com/frappe/erpnext/pull/24117))
+- Partial serial no return issue ([#24208](https://github.com/frappe/erpnext/pull/24208))
+- Import taxjar globally in the taxjar_integration module ([#24027](https://github.com/frappe/erpnext/pull/24027))
+- Payroll attendance error ([#23887](https://github.com/frappe/erpnext/pull/23887))
+- Loan application link on creating loan ([#23937](https://github.com/frappe/erpnext/pull/23937))
+- POS item search includes non stock items ([#23914](https://github.com/frappe/erpnext/pull/23914))
+- Paid amount in Sales Invoice POS return resets to 0 ([#24057](https://github.com/frappe/erpnext/pull/24057))
+- Fiscal year can be shorter than 12 months ([#23838](https://github.com/frappe/erpnext/pull/23838))
+- Loan repayment type option remove ([#23582](https://github.com/frappe/erpnext/pull/23582))
+- Item wise tax calculation ([#23744](https://github.com/frappe/erpnext/pull/23744))
+- Enabling track changes for stock settings ([#23982](https://github.com/frappe/erpnext/pull/23982))
+- Added link of bank reconciliation and clearance in accounting desk page ([#23809](https://github.com/frappe/erpnext/pull/23809))
+- Location data on Asset to use command(make_demo) ([#23825](https://github.com/frappe/erpnext/pull/23825))
+- Handle Account and Item None not found in Opening Invoice Creation Tool ([#23559](https://github.com/frappe/erpnext/pull/23559))
+- Multiple subcontracting issues ([#23662](https://github.com/frappe/erpnext/pull/23662))
+- Sequence id override with workstation column ([#23810](https://github.com/frappe/erpnext/pull/23810))
+- Leave policy dashboard fix and roles ([#24170](https://github.com/frappe/erpnext/pull/24170))
+- Scan barcode does not update barcode item field in sales order ([#24090](https://github.com/frappe/erpnext/pull/24090))
+- Item price duplicate checking ([#23408](https://github.com/frappe/erpnext/pull/23408))
+- Tax template update on supplier change for India ([#24060](https://github.com/frappe/erpnext/pull/24060))
+- Consumed qty logic for subcontracted raw materials ([#23314](https://github.com/frappe/erpnext/pull/23314))
+- Finance book not getting added in journal Entry of asset value adjustment ([#24100](https://github.com/frappe/erpnext/pull/24100))
+- Set proper state code in ewaybill JSON when GST category is SEZ ([#23953](https://github.com/frappe/erpnext/pull/23953))
+- Copying po no when mapping doc ([#23729](https://github.com/frappe/erpnext/pull/23729))
+- Duplicate items validation for POS Invoice when allow multiple items is disabled ([#23896](https://github.com/frappe/erpnext/pull/23896))
+- Do not allow Company as accounting dimension ([#23749](https://github.com/frappe/erpnext/pull/23749))
+- Validation for duplicate Tax Category ([#23978](https://github.com/frappe/erpnext/pull/23978))
+- Therapy plan and session fixes ([#23817](https://github.com/frappe/erpnext/pull/23817))
+- Pricing rule with transaction not working for additional product ([#24053](https://github.com/frappe/erpnext/pull/24053))
+- Inpatient Medication Order and Entry fixes ([#23799](https://github.com/frappe/erpnext/pull/23799))
+- Avoid using SQL query to get fiscal year dates ([#24050](https://github.com/frappe/erpnext/pull/24050))
+- Auto Statewise gst tax template ([#23832](https://github.com/frappe/erpnext/pull/23832))
+- On save sequence id column override with workstation ([#23812](https://github.com/frappe/erpnext/pull/23812))
+- Multiple pricing rules are not working on selling side ([#22711](https://github.com/frappe/erpnext/pull/22711))
+- Salary slip popup error ([#24192](https://github.com/frappe/erpnext/pull/24192))
+- Multiple pricing rule with margin type as Percentage is not working ([#24204](https://github.com/frappe/erpnext/pull/24204))
+- Allow statistical component in salary structure. ([#24424](https://github.com/frappe/erpnext/pull/24424))
+- Set current asset value before calculating difference amount ([#24119](https://github.com/frappe/erpnext/pull/24119))
+- To use Stock UoM in BOM Stock Report ([#24339](https://github.com/frappe/erpnext/pull/24339))
+- Accounting entries of asset when submitting purchase receipt ([#24191](https://github.com/frappe/erpnext/pull/24191))
+- Batch/Serial Selector for Scanned Batched Item ([#24338](https://github.com/frappe/erpnext/pull/24338))
+- Link timesheets with corresponding projects ([#24346](https://github.com/frappe/erpnext/pull/24346))
+- Material request wrong status issue ([#24019](https://github.com/frappe/erpnext/pull/24019))
+- UX issues in e-invoicing ([#24358](https://github.com/frappe/erpnext/pull/24358))
+- Company Wise Valuation Rate for RM in BOM ([#24324](https://github.com/frappe/erpnext/pull/24324))
+- Stock ageing should not take cancelled stock entries. ([#24437](https://github.com/frappe/erpnext/pull/24437))
+- Partial loan security unpledging ([#24252](https://github.com/frappe/erpnext/pull/24252))
+- Asset depreciation ledger ([#24226](https://github.com/frappe/erpnext/pull/24226))
+- Back Update from QC based on Batch No ([#24329](https://github.com/frappe/erpnext/pull/24329))
+- Fix for not having fiscal year while creating new company ([#24130](https://github.com/frappe/erpnext/pull/24130))
+- E-invoice print format not showing other charges ([#24474](https://github.com/frappe/erpnext/pull/24474))
+- Tax template update on customer address change ([#24146](https://github.com/frappe/erpnext/pull/24146))
+- Do not manufacture same serial no multiple times ([#24164](https://github.com/frappe/erpnext/pull/24164))
+- Ignore group cost center validation for period closing voucher ([#24375](https://github.com/frappe/erpnext/pull/24375))
+- Partial serial no return issue ([#24207](https://github.com/frappe/erpnext/pull/24207))
+- GSTR-1 double entry issue ([#24376](https://github.com/frappe/erpnext/pull/24376))
+- Not able to create dunning from sales invoice ([#24349](https://github.com/frappe/erpnext/pull/24349))
+- Set company in leave allocation and leave ledger entry ([#24296](https://github.com/frappe/erpnext/pull/24296))
+- Allow leave policy assignment to be canceled. ([#24265](https://github.com/frappe/erpnext/pull/24265))
+- Removed all day event from shift assignment calendar ([#24397](https://github.com/frappe/erpnext/pull/24397))
+- Tax calculation on salary slip for the first month ([#24272](https://github.com/frappe/erpnext/pull/24272))
+- Validate tax template for tax category ([#24402](https://github.com/frappe/erpnext/pull/24402))
+- Numeric/Non-numeric QI UX ([#24517](https://github.com/frappe/erpnext/pull/24517))
+- Finished good produced qty validation ([#24220](https://github.com/frappe/erpnext/pull/24220))
+- Incorrect serial no in the subcontracted purchase receipt ([#24354](https://github.com/frappe/erpnext/pull/24354))
+- Don't validate warehouse values between Material Request and Stock Entry ([#24294](https://github.com/frappe/erpnext/pull/24294))
+- Don't cancel job card if manufacturing entry has made ([#24063](https://github.com/frappe/erpnext/pull/24063))
+- Subscription prepaid date validation ([#24356](https://github.com/frappe/erpnext/pull/24356))
+- Payment Period based on invoice date report fix/refactor ([#24378](https://github.com/frappe/erpnext/pull/24378))
+- Drop ship partial order fixed ([#24072](https://github.com/frappe/erpnext/pull/24072))
+- Payment entry multi-currency issue ([#24332](https://github.com/frappe/erpnext/pull/24332))
+- Multiple pricing rule issue ([#24515](https://github.com/frappe/erpnext/pull/24515))
+- Last purchase rate not updating when voucher cancelled if only one voucher is present ([#24322](https://github.com/frappe/erpnext/pull/24322))
+- Do not cancel reference document on Quality Inspection cancellation ([#24197](https://github.com/frappe/erpnext/pull/24197))
+- Refactored fetching & validating address from erpnext rather than gst portal ([#24297](https://github.com/frappe/erpnext/pull/24297))
+- Opportunity Status fix ([#22944](https://github.com/frappe/erpnext/pull/22944))
+- Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644))
+- Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649))
+- Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580))
+- Repost incompleted backdated transactions ([#24991](https://github.com/frappe/erpnext/pull/24991))
+- Unequal debit and credit issue on RCM Invoice ([#24838](https://github.com/frappe/erpnext/pull/24838))
+- Period list for exponential smoothing forecasting report ([#24983](https://github.com/frappe/erpnext/pull/24983))
+- POS Opening Entry with empty balance detail rows ([#24891](https://github.com/frappe/erpnext/pull/24891))
+- Use account_name only in consolidated report ([#24840](https://github.com/frappe/erpnext/pull/24840))
+- Validation of job card in stock entry ([#24882](https://github.com/frappe/erpnext/pull/24882))
+- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24918](https://github.com/frappe/erpnext/pull/24918))
+- TDS check getting checked after reload ([#24973](https://github.com/frappe/erpnext/pull/24973))
+- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
+- Allow zero valuation in stock reconciliation ([#24985](https://github.com/frappe/erpnext/pull/24985))
+- Simplified logic for additional salary ([#24907](https://github.com/frappe/erpnext/pull/24907))
+- Allow to select item code in batch naming ([#24825](https://github.com/frappe/erpnext/pull/24825))
+- Membership renewal validation (#24963) ([#24964](https://github.com/frappe/erpnext/pull/24964))
+</details>
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 73276f3..33fbf1c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -517,6 +517,7 @@
frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s
and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name))
+ @frappe.whitelist()
def apply_shipping_rule(self):
if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
@@ -537,6 +538,7 @@
return {}
+ @frappe.whitelist()
def set_advances(self):
"""Returns list of advances against Account, Party, Reference"""
@@ -657,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
@@ -921,7 +924,8 @@
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
- d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount'))
+ d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
+ d.outstanding = d.payment_amount
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -1236,18 +1240,24 @@
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
+ term_details.discount_type = term.discount_type
+ term_details.discount = term.discount
+ # term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount
+ term_details.outstanding = term_details.payment_amount
+ term_details.mode_of_payment = term.mode_of_payment
+
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
+ term_details.discount_date = get_discount_date(term, bill_date)
elif posting_date:
term_details.due_date = get_due_date(term, posting_date)
+ term_details.discount_date = get_discount_date(term, posting_date)
if getdate(term_details.due_date) < getdate(posting_date):
term_details.due_date = posting_date
- term_details.mode_of_payment = term.mode_of_payment
return term_details
-
def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
date = bill_date or posting_date
@@ -1259,6 +1269,16 @@
due_date = add_months(get_last_day(date), term.credit_months)
return due_date
+def get_discount_date(term, posting_date=None, bill_date=None):
+ discount_validity = None
+ date = bill_date or posting_date
+ if term.discount_validity_based_on == "Day(s) after invoice date":
+ discount_validity = add_days(date, term.discount_validity)
+ elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
+ discount_validity = add_days(get_last_day(date), term.discount_validity)
+ elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
+ discount_validity = add_months(get_last_day(date), term.discount_validity)
+ return discount_validity
def get_supplier_block_status(party_name):
"""
@@ -1317,25 +1337,63 @@
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
item = frappe.get_doc("Item", trans_item.get('item_code'))
+
for field in ("item_code", "item_name", "description", "item_group"):
- child_item.update({field: item.get(field)})
+ child_item.update({field: item.get(field)})
+
date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
+ child_item.stock_uom = item.stock_uom
child_item.uom = trans_item.get("uom") or item.stock_uom
+ child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
+
if child_doctype == "Purchase Order Item":
- child_item.base_rate = 1 # Initiallize value will update in parent validation
- child_item.base_amount = 1 # Initiallize value will update in parent validation
+ # Initialized value will update in parent validation
+ child_item.base_rate = 1
+ child_item.base_amount = 1
if child_doctype == "Sales Order Item":
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
.format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
+
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
return child_item
+def validate_child_on_delete(row, parent):
+ """Check if partially transacted item (row) is being deleted."""
+ if parent.doctype == "Sales Order":
+ if flt(row.delivered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(row.idx, row.item_code))
+ if flt(row.work_order_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(row.idx, row.item_code))
+ if flt(row.ordered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(row.idx, row.item_code))
+
+ if parent.doctype == "Purchase Order" and flt(row.received_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(row.idx, row.item_code))
+
+ if flt(row.billed_amt):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(row.idx, row.item_code))
+
+def update_bin_on_delete(row, doctype):
+ """Update bin for deleted item (row)."""
+ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty, get_ordered_qty, get_indented_qty
+ qty_dict = {}
+
+ if doctype == "Sales Order":
+ qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
+ else:
+ if row.material_request_item:
+ qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)
+
+ qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
+
+ update_bin_qty(row.item_code, row.warehouse, qty_dict)
+
def validate_and_delete_children(parent, data):
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
@@ -1344,23 +1402,17 @@
deleted_children.append(item)
for d in deleted_children:
- if parent.doctype == "Sales Order":
- if flt(d.delivered_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
- if flt(d.work_order_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(d.idx, d.item_code))
- if flt(d.ordered_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(d.idx, d.item_code))
-
- if parent.doctype == "Purchase Order" and flt(d.received_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
-
- if flt(d.billed_amt):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
-
+ validate_child_on_delete(d, parent)
d.cancel()
d.delete()
+ # need to update ordered qty in Material Request first
+ # bin uses Material Request Items to recalculate & update
+ parent.update_prevdoc_status()
+
+ for d in deleted_children:
+ update_bin_on_delete(d, parent.doctype)
+
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'):
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 81f0ad3..c0c1315 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -325,7 +325,7 @@
and status not in ("Stopped", "Closed") %(fcond)s
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
- or `tabDelivery Note`.grand_total = 0
+ or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
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 e329b32..9fae494 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -113,10 +113,12 @@
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
- if not item.discount_amount:
+
+ if item.discount_amount and not item.discount_percentage:
+ item.rate = item.rate_with_margin - item.discount_amount
+ else:
item.discount_amount = item.rate_with_margin - item.rate
- elif not item.discount_percentage:
- item.rate -= item.discount_amount
+
elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@@ -147,7 +149,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"]
@@ -287,10 +291,13 @@
# set precision in the last item iteration
if n == len(self.doc.get("items")) - 1:
self.round_off_totals(tax)
+ self._set_in_company_currency(tax,
+ ["tax_amount", "tax_amount_after_discount_amount"])
+
+ self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
- self._set_in_company_currency(tax,
- ["total", "tax_amount", "tax_amount_after_discount_amount"])
+ self._set_in_company_currency(tax, ["total"])
# adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
@@ -337,18 +344,11 @@
elif tax.charge_type == "On Item Quantity":
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
- def get_final_current_tax_amount(self, tax, current_tax_amount):
- # Some countries need individual tax components to be rounded
- # Handeled via regional doctypess
- if tax.account_head in frappe.flags.round_off_applicable_accounts:
- current_tax_amount = round(current_tax_amount, 0)
- return current_tax_amount
-
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -359,10 +359,20 @@
tax.item_wise_tax_detail[key] = [tax_rate,flt(item_wise_tax_amount)]
def round_off_totals(self, tax):
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ tax.tax_amount = round(tax.tax_amount, 0)
+ tax.tax_amount_after_discount_amount = round(tax.tax_amount_after_discount_amount, 0)
+
tax.tax_amount = flt(tax.tax_amount, tax.precision("tax_amount"))
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount,
tax.precision("tax_amount"))
+ def round_off_base_values(self, tax):
+ # Round off to nearest integer based on regional settings
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ tax.base_tax_amount = round(tax.base_tax_amount, 0)
+ tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
+
def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
@@ -440,8 +450,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:
@@ -808,4 +819,4 @@
def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount"))
- d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))
\ No newline at end of file
+ d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 801c405..ecf041e 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -25,7 +25,7 @@
if not filters: filters = []
- if doctype in ['Supplier Quotation', 'Purchase Invoice', 'Quotation']:
+ if doctype in ['Supplier Quotation', 'Purchase Invoice']:
filters.append((doctype, 'docstatus', '<', 2))
else:
filters.append((doctype, 'docstatus', '=', 1))
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/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index 377e061..d8c6fb4 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -11,7 +11,8 @@
from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document):
- def get_authorization_url(self):
+ @frappe.whitelist()
+ def get_authorization_url(self):
params = urlencode({
"response_type":"code",
"client_id": self.consumer_key,
@@ -35,7 +36,7 @@
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
-
+
response = self.http_post(url=url, data=body, headers=headers)
response = frappe.parse_json(response.content.decode())
self.db_set("access_token", response["access_token"])
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 0522ace..23ad98a 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -85,6 +85,7 @@
self.opportunity_from = "Lead"
self.party_name = lead_name
+ @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_active_quotation():
frappe.db.set(self, 'status', 'Lost')
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 976a23d..1e1beab 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -11,6 +11,7 @@
from tweepy.error import TweepError
class TwitterSettings(Document):
+ @frappe.whitelist()
def get_authorize_url(self):
callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url())
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
@@ -21,12 +22,12 @@
frappe.msgprint(_("Error! Failed to get request token."))
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
-
+
def get_access_token(self, oauth_token, oauth_verifier):
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- auth.request_token = {
+ auth.request_token = {
'oauth_token' : oauth_token,
- 'oauth_token_secret' : oauth_verifier
+ 'oauth_token_secret' : oauth_verifier
}
try:
@@ -50,10 +51,10 @@
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
def get_api(self, access_token, access_token_secret):
- # authentication of consumer key and secret
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- # authentication of access token and secret
- auth.set_access_token(access_token, access_token_secret)
+ # authentication of consumer key and secret
+ auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ # authentication of access token and secret
+ auth.set_access_token(access_token, access_token_secret)
return tweepy.API(auth)
@@ -64,7 +65,7 @@
if media:
media_id = self.upload_image(media)
return self.send_tweet(text, media_id)
-
+
def upload_image(self, media):
media = get_file_path(media)
api = self.get_api(self.access_token, self.access_token_secret)
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 97c29ab..6a0dcf4 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -13,6 +13,7 @@
class CourseSchedulingTool(Document):
+ @frappe.whitelist()
def schedule_course(self):
"""Creates course schedules as per specified parameters"""
diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.py b/erpnext/education/doctype/fee_schedule/fee_schedule.py
index 1543acd..0b025c7 100644
--- a/erpnext/education/doctype/fee_schedule/fee_schedule.py
+++ b/erpnext/education/doctype/fee_schedule/fee_schedule.py
@@ -52,6 +52,7 @@
self.grand_total = no_of_students*self.total_amount
self.grand_total_in_words = money_in_words(self.grand_total)
+ @frappe.whitelist()
def create_fees(self):
self.db_set("fee_creation_status", "In Process")
frappe.publish_realtime("fee_schedule_progress",
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index d18c0f9..b282bab 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -91,6 +91,8 @@
(fee, fee) for fee in fee_list]
msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list)))
+
+ @frappe.whitelist()
def get_courses(self):
return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1)
diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
index 8180102..5833b67 100644
--- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
+++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
@@ -14,6 +14,7 @@
academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd'))
self.set_onload("academic_term_reqd", academic_term_reqd)
+ @frappe.whitelist()
def get_students(self):
students = []
if not self.get_students_from:
@@ -49,6 +50,7 @@
else:
frappe.throw(_("No students Found"))
+ @frappe.whitelist()
def enroll_students(self):
total = len(self.students)
for i, stud in enumerate(self.students):
diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
index d7645e3..dc8667e 100644
--- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
+++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
@@ -9,6 +9,7 @@
from erpnext.education.doctype.student_group.student_group import get_students
class StudentGroupCreationTool(Document):
+ @frappe.whitelist()
def get_courses(self):
group_list = []
@@ -42,6 +43,7 @@
return group_list
+ @frappe.whitelist()
def create_student_groups(self):
if not self.courses:
frappe.throw(_("""No Student Groups created."""))
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index b571802..fdfaa1b 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -59,9 +59,10 @@
request_amounts.append(amount)
else:
request_amounts = [request_amount]
-
+
return request_amounts
+ @frappe.whitelist()
def get_account_balance_info(self):
payload = dict(
reference_doctype="Mpesa Settings",
@@ -198,7 +199,7 @@
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt)
-
+
return mpesa_receipts, completed_payments
def get_account_balance(request_payload):
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 21f6fee..16c6573 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -15,6 +15,7 @@
class PlaidSettings(Document):
@staticmethod
+ @frappe.whitelist()
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index 3c90637..e2243ea 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -23,14 +23,9 @@
doc.cancel()
doc.delete()
- for ba in frappe.get_all("Bank Account"):
- frappe.get_doc("Bank Account", ba.name).delete()
-
- for at in frappe.get_all("Bank Account Type"):
- frappe.get_doc("Bank Account Type", at.name).delete()
-
- for ast in frappe.get_all("Bank Account Subtype"):
- frappe.get_doc("Bank Account Subtype", ast.name).delete()
+ for doctype in ("Bank Account", "Bank Account Type", "Bank Account Subtype"):
+ for d in frappe.get_all(doctype):
+ frappe.delete_doc(doctype, d.name, force=True)
def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
index 96a533e..866ea66 100644
--- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
@@ -54,6 +54,7 @@
self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
+ @frappe.whitelist()
def migrate(self):
frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long")
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
index 5f471ab..6bec301 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
@@ -22,7 +22,7 @@
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
# use the fixture data
- import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
+ import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 462685f..907a223 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -594,18 +594,22 @@
frappe.db.set_value("Price List", "Tally Price List", "enabled", 0)
frappe.flags.in_migrate = False
+ @frappe.whitelist()
def process_master_data(self):
self.set_status("Processing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def import_master_data(self):
self.set_status("Importing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def process_day_book_data(self):
self.set_status("Processing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def import_day_book_data(self):
self.set_status("Importing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
index 325c209..cbf89ee 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
@@ -54,6 +54,7 @@
def set_title(self):
self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100]
+ @frappe.whitelist()
def complete_procedure(self):
if self.consume_stock and self.items:
stock_entry = make_stock_entry(self)
@@ -96,6 +97,7 @@
if self.consume_stock and self.items:
return stock_entry
+ @frappe.whitelist()
def start_procedure(self):
allow_start = self.set_actual_qty()
if allow_start:
@@ -116,6 +118,7 @@
return allow_start
+ @frappe.whitelist()
def make_material_receipt(self, submit=False):
stock_entry = frappe.new_doc('Stock Entry')
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
index e731908..3a299ed 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -14,6 +14,7 @@
def validate(self):
self.validate_medication_orders()
+ @frappe.whitelist()
def get_medication_orders(self):
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
index 33cbbec..b379e98 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
@@ -57,6 +57,7 @@
self.db_set('status', status)
+ @frappe.whitelist()
def add_order_entries(self, order):
if order.get('drug_code'):
dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
index a21caca..21776d2 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
@@ -81,15 +81,8 @@
self.ip_record.reload()
discharge_patient(self.ip_record)
- for entry in frappe.get_all('Inpatient Medication Entry'):
- doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
- doc.cancel()
- doc.delete()
-
- for entry in frappe.get_all('Inpatient Medication Order'):
- doc = frappe.get_doc('Inpatient Medication Order', entry.name)
- doc.cancel()
- doc.delete()
+ for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]:
+ frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def create_dosage_form():
if not frappe.db.exists('Dosage Form', 'Tablet'):
diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
index 2934316..f4d1eaf 100644
--- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
@@ -53,12 +53,15 @@
+ """ <b><a href="/app/Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name))
frappe.throw(msg)
+ @frappe.whitelist()
def admit(self, service_unit, check_in, expected_discharge=None):
admit_patient(self, service_unit, check_in, expected_discharge)
+ @frappe.whitelist()
def discharge(self):
discharge_patient(self)
+ @frappe.whitelist()
def transfer(self, service_unit, check_in, leave_from):
if leave_from:
patient_leave_service_unit(self, check_in, leave_from)
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 8603f97..789d452 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -111,6 +111,7 @@
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
+ @frappe.whitelist()
def invoice_patient_registration(self):
if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'):
company = frappe.defaults.get_user_default('company')
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index 1f76cd6..cdd4ad3 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -113,6 +113,7 @@
if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
+ @frappe.whitelist()
def get_therapy_types(self):
if not self.therapy_plan:
return
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/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
index 2e8c994..887d58a 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
@@ -34,6 +34,7 @@
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
+ @frappe.whitelist()
def get_doctype_fields(self, document_type, fields):
multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields
@@ -49,6 +50,7 @@
return multicheck_fields
+ @frappe.whitelist()
def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', {
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/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index ac01c60..e209660 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -33,6 +33,7 @@
self.db_set('total_sessions', total_sessions)
self.db_set('total_sessions_completed', total_sessions_completed)
+ @frappe.whitelist()
def set_therapy_details_from_template(self):
# Add therapy types in the child table
self.set('therapy_plan_details', [])
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9b9a0da..98d5966 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -195,6 +195,10 @@
{"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
]
+has_upload_permission = {
+ "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"
+}
+
has_website_permission = {
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
@@ -256,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": [
@@ -278,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/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 18a4fe5..f3b8a79 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -35,7 +35,8 @@
and docstatus != 2
""", (self.employee, getdate(self.attendance_date), self.name))
if res:
- frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee))
+ frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
+ frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def check_leave_record(self):
leave_record = frappe.db.sql("""
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 d0e7d05..ed7d588 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -8,7 +8,7 @@
from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \
- set_user_permission_if_allowed, has_permission
+ set_user_permission_if_allowed, has_permission, get_doc_permissions
from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
@@ -66,7 +66,7 @@
def validate_user_details(self):
data = frappe.db.get_value('User',
self.user_id, ['enabled', 'user_image'], as_dict=1)
- if data.get("user_image"):
+ if data.get("user_image") and self.image == '':
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
@@ -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."))
@@ -501,3 +513,10 @@
'allow': 'Employee',
'for_value': employee_name
})
+
+def has_upload_permission(doc, ptype='read', user=None):
+ if not user:
+ user = frappe.session.user
+ if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
+ return True
+ return doc.user_id == user
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json
index cf6b540..ea25aa7 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.json
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.json
@@ -181,7 +181,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -201,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 12:01:55.980721",
+ "modified": "2021-03-31 22:31:53.746659",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index bf893d5..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')
@@ -211,6 +214,7 @@
self.total_claimed_amount += flt(d.amount)
self.total_sanctioned_amount += flt(d.sanctioned_amount)
+ @frappe.whitelist()
def calculate_taxes(self):
self.total_taxes_and_charges = 0
for tax in self.taxes:
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_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 69d605d..11302ca 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -99,6 +99,7 @@
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name),
BackDatedAllocationError)
+ @frappe.whitelist()
def set_total_leaves_allocated(self):
self.unused_leaves = get_carry_forwarded_leaves(self.employee,
self.leave_type, self.from_date, self.carry_forward)
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 b335c48..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"]
@@ -56,6 +57,7 @@
@classmethod
def setUpClass(cls):
set_leave_approver()
+ frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
frappe.set_user("Administrator")
@@ -230,8 +232,9 @@
def test_optional_leave(self):
leave_period = get_leave_period()
today = nowdate()
- from datetime import date
holiday_list = 'Test Holiday List for Optional Holiday'
+ optional_leave_date = add_days(today, 7)
+
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
doctype = 'Holiday List',
@@ -239,7 +242,7 @@
from_date = add_months(today, -6),
to_date = add_months(today, 6),
holidays = [
- dict(holiday_date = today, description = 'Test')
+ dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
employee = get_employee()
@@ -255,7 +258,7 @@
allocate_leaves(employee, leave_period, leave_type, 10)
- date = add_days(today, - 1)
+ date = add_days(today, 6)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -270,14 +273,14 @@
# can only apply on optional holidays
self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
- leave_application.from_date = today
- leave_application.to_date = today
+ leave_application.from_date = optional_leave_date
+ leave_application.to_date = optional_leave_date
leave_application.status = "Approved"
leave_application.insert()
leave_application.submit()
# check leave balance is reduced
- self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9)
+ self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
def test_leaves_allowed(self):
employee = get_employee()
@@ -341,7 +344,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
@@ -363,7 +366,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertTrue(leave_application.insert())
@@ -393,7 +396,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
@@ -508,7 +511,7 @@
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
leave_application.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
@@ -540,7 +543,7 @@
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
leave_application.submit()
@@ -565,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_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
index 83eeae3..1f6c03f 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
@@ -130,7 +130,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -155,7 +154,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:56:06.777241",
+ "modified": "2021-03-31 22:32:55.492327",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 4c1a465..e041b7f 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -63,6 +63,7 @@
frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days)
self.create_leave_ledger_entry(submit=False)
+ @frappe.whitelist()
def get_leave_details_for_encashment(self):
salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
if not salary_structure:
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 63559c4..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(
@@ -52,7 +52,9 @@
ledger.update(args)
if submit:
- frappe.get_doc(ledger).submit()
+ doc = frappe.get_doc(ledger)
+ doc.flags.ignore_permissions = 1
+ doc.submit()
else:
delete_ledger_entry(ledger)
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 4064c56..462b81d 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -36,6 +36,7 @@
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
+ @frappe.whitelist()
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
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/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index 054e7e3..d5fdda8 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -15,6 +15,7 @@
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class ShiftType(Document):
+ @frappe.whitelist()
def process_auto_attendance(self):
if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
return
diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
index e961114..303c829 100644
--- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
+++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
@@ -31,7 +31,7 @@
"fieldtype": "Link",
"fieldname": "job_opening",
"options": "Job Opening",
- "width": 100
+ "width": 105
},
{
"label": _("Job Applicant"),
@@ -44,13 +44,13 @@
"label": _("Applicant name"),
"fieldtype": "data",
"fieldname": "applicant_name",
- "width": 120
+ "width": 130
},
{
"label": _("Application Status"),
"fieldtype": "Data",
"fieldname": "application_status",
- "width": 100
+ "width": 150
},
{
"label": _("Job Offer"),
@@ -187,4 +187,4 @@
else:
ja_joff_map[offer.job_applicant].append(offer)
- return ja_joff_map
\ No newline at end of file
+ return ja_joff_map
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.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
index 102bc0d..99b5c72 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "LM-LSS-.#####",
"creation": "2019-09-06 11:33:34.709540",
"doctype": "DocType",
@@ -14,6 +15,7 @@
"shortfall_amount",
"column_break_8",
"security_value",
+ "shortfall_percentage",
"section_break_8",
"process_loan_security_shortfall"
],
@@ -85,10 +87,18 @@
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shortfall_percentage",
+ "fieldtype": "Percent",
+ "label": "Shortfall Percentage",
+ "read_only": 1
}
],
"in_create": 1,
- "modified": "2019-10-24 06:24:26.128997",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-04-01 08:13:43.263772",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Shortfall",
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 b5e7898..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)
@@ -22,7 +22,9 @@
if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
"status": "Completed",
- "shortfall_amount": loan_security_shortfall.shortfall_amount})
+ "shortfall_amount": loan_security_shortfall.shortfall_amount,
+ "shortfall_percentage": 0
+ })
else:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)
@@ -65,7 +67,8 @@
outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
- outstanding_amount = loan.disbursed_amount
+ outstanding_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
+ - flt(loan.total_principal_paid)
pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = ''
@@ -81,14 +84,15 @@
if current_ratio > ltv_ratio:
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount,
- process_loan_security_shortfall)
+ current_ratio, process_loan_security_shortfall)
elif loan_shortfall_map.get(loan.name):
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
if shortfall_amount <= 0:
shortfall = loan_shortfall_map.get(loan.name)
update_pending_shortfall(shortfall)
-def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
+def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, shortfall_ratio,
+ process_loan_security_shortfall):
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
if existing_shortfall:
@@ -101,6 +105,7 @@
ltv_shortfall.loan_amount = loan_amount
ltv_shortfall.security_value = security_value
ltv_shortfall.shortfall_amount = shortfall_amount
+ ltv_shortfall.shortfall_percentage = shortfall_ratio
ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall
ltv_shortfall.save()
@@ -114,6 +119,7 @@
frappe.db.set_value("Loan Security Shortfall", shortfall,
{
"status": "Completed",
- "shortfall_amount": 0
+ "shortfall_amount": 0,
+ "shortfall_percentage": 0
})
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/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
index 0f72c3c..2a74a1e 100644
--- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -63,9 +63,11 @@
currency = erpnext.get_company_currency(filters.get('company'))
for loan in loan_details:
+ total_payment = loan.total_payment if loan.status == 'Disbursed' else loan.disbursed_amount
+
loan.update({
"sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
- "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \
+ "principal_outstanding": flt(total_payment) - flt(loan.total_principal_paid) \
- flt(loan.total_interest_payable) - flt(loan.written_off_amount),
"total_repayment": flt(payments.get(loan.loan)),
"accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index cba6a2d..0aefe19 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -12,6 +12,7 @@
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class MaintenanceSchedule(TransactionBase):
+ @frappe.whitelist()
def generate_schedule(self):
self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 03beedb..979f7ca 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -113,6 +113,7 @@
return item
+ @frappe.whitelist()
def get_routing(self):
if self.routing:
self.set("operations", [])
@@ -145,6 +146,7 @@
if not item.get(r):
item.set(r, ret[r])
+ @frappe.whitelist()
def get_bom_material_detail(self, args=None):
""" Get raw material details like uom, desc and rate"""
if not args:
@@ -210,6 +212,7 @@
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
+ @frappe.whitelist()
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
if self.docstatus == 2:
return
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3239478..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)
@@ -134,7 +146,13 @@
bom.items[0].conversion_factor = 6
bom.insert()
- reset_item_valuation_rate(item_code='_Test Item', qty=200, rate=200)
+ reset_item_valuation_rate(
+ item_code='_Test Item',
+ warehouse_list=frappe.get_all("Warehouse",
+ {"is_group":0, "company": bom.company}, pluck="name"),
+ qty=200,
+ rate=200
+ )
bom.update_cost()
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 7aaf2a0..fb26062 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
@@ -164,6 +166,7 @@
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
})
+ @frappe.whitelist()
def get_required_items(self):
if not self.get('work_order'):
return
@@ -430,6 +433,7 @@
def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse
+ target.conversion_factor = 1
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 15ec620..288c1d0 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -25,6 +25,16 @@
}
});
+ frm.set_query('material_request', 'material_requests', function() {
+ return {
+ filters: {
+ material_request_type: "Manufacture",
+ docstatus: 1,
+ status: ["!=", "Stopped"],
+ }
+ };
+ });
+
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
return {
query: "erpnext.controllers.queries.item_query",
@@ -370,4 +380,4 @@
['Sales Order','docstatus', '=' ,1]
]
}
-};
\ No newline at end of file
+};
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 109c8b5..cef2d8b 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -29,6 +29,7 @@
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
+ @frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""
open_so = get_sales_orders(self)
@@ -50,6 +51,7 @@
'grand_total': data.base_grand_total
})
+ @frappe.whitelist()
def get_pending_material_requests(self):
""" Pull Material Requests that are pending based on criteria selected"""
mr_filter = item_filter = ""
@@ -68,7 +70,7 @@
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr_item.parent = mr.name
and mr.material_request_type = "Manufacture"
- and mr.docstatus = 1 and mr.company = %(company)s
+ and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1))
@@ -92,6 +94,7 @@
'material_request_date': data.transaction_date
})
+ @frappe.whitelist()
def get_items(self):
if self.get_items_from == "Sales Order":
self.get_so_items()
@@ -219,6 +222,7 @@
filters = {'docstatus': 0, 'production_plan': ("=", self.name)}):
frappe.delete_doc('Work Order', d.name)
+ @frappe.whitelist()
def set_status(self, close=None):
self.status = {
0: 'Draft',
@@ -302,6 +306,7 @@
return item_dict
+ @frappe.whitelist()
def make_work_order(self):
wo_list = []
self.validate_data()
@@ -367,6 +372,7 @@
except OverProductionError:
pass
+ @frappe.whitelist()
def make_material_request(self):
'''Create Material Requests grouped by Sales Order and Material Request Type'''
material_request_list = []
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/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 73d05a6..6a38dcf 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -13,8 +13,15 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.item_code = "Test Routing Item - A"
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql('delete from tabBOM where item=%s', cls.item_code)
+
def test_sequence_id(self):
- item_code = "Test Routing Item - A"
operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}]
@@ -22,8 +29,8 @@
setup_operations(operations)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
- bom_doc = setup_bom(item_code=item_code, routing=routing_doc.name)
- wo_doc = make_wo_order_test_record(production_item = item_code, bom_no=bom_doc.name)
+ bom_doc = setup_bom(item_code=self.item_code, routing=routing_doc.name)
+ wo_doc = make_wo_order_test_record(production_item = self.item_code, bom_no=bom_doc.name)
for row in routing_doc.operations:
self.assertEqual(row.sequence_id, row.idx)
@@ -74,7 +81,7 @@
})
if not args.raw_materials:
- if not frappe.db.exists('Item', "Test Extra Item 1"):
+ if not frappe.db.exists('Item', "Test Extra Item N-1"):
make_item("Test Extra Item N-1", {
'is_stock_item': 1,
})
@@ -88,4 +95,4 @@
else:
bom_doc = frappe.get_doc("BOM", name)
- return bom_doc
\ No newline at end of file
+ return bom_doc
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 08291d1..6b1fafe 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -371,14 +371,14 @@
def test_job_card(self):
stock_entries = []
- data = frappe.get_cached_value('BOM',
- {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
+ bom = frappe.get_doc('BOM', {
+ 'docstatus': 1,
+ 'with_operations': 1,
+ 'company': '_Test Company'
+ })
- bom, bom_item = data
-
- bom_doc = frappe.get_doc('BOM', bom)
- work_order = make_wo_order_test_record(item=bom_item, qty=1,
- bom_no=bom, source_warehouse="_Test Warehouse - _TC")
+ work_order = make_wo_order_test_record(item=bom.item, qty=1,
+ bom_no=bom.name, source_warehouse="_Test Warehouse - _TC")
for row in work_order.required_items:
stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
@@ -390,14 +390,14 @@
stock_entries.append(ste)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
- self.assertEqual(len(job_cards), len(bom_doc.operations))
+ self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {
- "from_time": now(),
- "hours": i,
- "to_time": add_to_date(now(), i),
+ "from_time": add_to_date(None, i),
+ "hours": 1,
+ "to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.submit()
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 3d64ad4..8507f5e 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -509,6 +509,7 @@
stock_bin = get_bin(d.item_code, d.source_warehouse)
stock_bin.update_reserved_qty_for_production()
+ @frappe.whitelist()
def get_items_and_operations_from_bom(self):
self.set_required_items()
self.set_work_order_operations()
@@ -613,6 +614,7 @@
item.db_set('consumed_qty', flt(consumed_qty), update_modified=False)
+ @frappe.whitelist()
def make_bom(self):
data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse
from `tabStock Entry Detail` sed, `tabStock Entry` se
diff --git a/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json
index 7b5747e..7317152 100644
--- a/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json
@@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing",
"idx": 0,
"is_complete": 0,
- "modified": "2020-07-08 14:05:56.197563",
+ "modified": "2020-06-29 20:25:36.899106",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -53,4 +53,4 @@
"subtitle": "Products, Raw Materials, BOM, Work Order, and more.",
"success_message": "Manufacturing module is all set up!",
"title": "Let's Set Up the Manufacturing Module."
-}
\ No newline at end of file
+}
diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py
index 6a2a06d..4fd1a30 100644
--- a/erpnext/non_profit/doctype/donation/donation.py
+++ b/erpnext/non_profit/doctype/donation/donation.py
@@ -42,7 +42,7 @@
self.load_from_db()
self.create_payment_entry()
- def create_payment_entry(self):
+ def create_payment_entry(self, date=None):
settings = frappe.get_doc('Non Profit Settings')
if not settings.automate_donation_payment_entries:
return
@@ -58,8 +58,9 @@
frappe.flags.ignore_account_permission = False
pe.paid_from = settings.donation_debit_account
pe.paid_to = settings.donation_payment_account
+ pe.posting_date = date or getdate()
pe.reference_no = self.name
- pe.reference_date = getdate()
+ pe.reference_date = date or getdate()
pe.flags.ignore_mandatory = True
pe.insert()
pe.submit()
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 3ba2ee7..efc072e 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -53,6 +53,7 @@
return subscription
+ @frappe.whitelist()
def make_customer_and_link(self):
if self.customer:
frappe.msgprint(_("A customer is already linked to this Member"))
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 52447e4..e8ae618 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -74,6 +74,7 @@
self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
+ @frappe.whitelist()
def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@@ -130,6 +131,7 @@
pe.save()
pe.submit()
+ @frappe.whitelist()
def send_acknowlement(self):
settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email:
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
index cff92b4..4c4ca98 100644
--- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
@@ -19,7 +19,7 @@
};
});
- frm.set_query("debit_account", function() {
+ frm.set_query("membership_debit_account", function() {
return {
filters: {
"account_type": "Receivable",
@@ -29,6 +29,16 @@
};
});
+ frm.set_query("donation_debit_account", function() {
+ return {
+ filters: {
+ "account_type": "Receivable",
+ "is_group": 0,
+ "company": frm.doc.donation_company
+ }
+ };
+ });
+
frm.set_query("membership_payment_account", function () {
var account_types = ["Bank", "Cash"];
return {
@@ -40,6 +50,17 @@
};
});
+ frm.set_query("donation_payment_account", function () {
+ var account_types = ["Bank", "Cash"];
+ return {
+ filters: {
+ "account_type": ["in", account_types],
+ "is_group": 0,
+ "company": frm.doc.donation_company
+ }
+ };
+ });
+
let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
index 108554c..a84cc2c 100644
--- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
@@ -9,6 +9,7 @@
from frappe.model.document import Document
class NonProfitSettings(Document):
+ @frappe.whitelist()
def generate_webhook_secret(self, field="membership_webhook_secret"):
key = frappe.generate_hash(length=20)
self.set(field, key)
@@ -21,6 +22,7 @@
_("Webhook Secret")
)
+ @frappe.whitelist()
def revoke_key(self, key):
self.set(key, None)
self.save()
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 46f0d4a..14f1ab8 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -99,7 +99,7 @@
execute:frappe.delete_doc("DocType", "Purchase Request Item")
erpnext.patches.v4_2.recalculate_bom_cost
erpnext.patches.v4_2.fix_gl_entries_for_stock_transactions
-erpnext.patches.v4_2.update_requested_and_ordered_qty
+erpnext.patches.v4_2.update_requested_and_ordered_qty #2021-03-31
execute:frappe.rename_doc("DocType", "Support Ticket", "Issue", force=True)
erpnext.patches.v4_4.make_email_accounts
execute:frappe.delete_doc("DocType", "Contact Control")
@@ -208,7 +208,7 @@
erpnext.patches.v5_7.item_template_attributes
execute:frappe.delete_doc_if_exists("DocType", "Manage Variants")
execute:frappe.delete_doc_if_exists("DocType", "Manage Variants Item")
-erpnext.patches.v4_2.repost_reserved_qty #2016-04-15
+erpnext.patches.v4_2.repost_reserved_qty #2021-03-31
erpnext.patches.v5_4.update_purchase_cost_against_project
erpnext.patches.v5_8.update_order_reference_in_return_entries
erpnext.patches.v5_8.add_credit_note_print_heading
@@ -752,13 +752,22 @@
erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
+erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
-erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
+erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.setup_uae_vat_fields
execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
+erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v13_0.rename_discharge_date_in_ip_record
+erpnext.patches.v12_0.create_taxable_value_field
+erpnext.patches.v12_0.add_gst_category_in_delivery_note
+erpnext.patches.v12_0.purchase_receipt_status
+erpnext.patches.v13_0.fix_non_unique_represents_company
+erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
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/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py
new file mode 100644
index 0000000..1a99b31
--- /dev/null
+++ b/erpnext/patches/v12_0/purchase_receipt_status.py
@@ -0,0 +1,30 @@
+""" This patch fixes old purchase receipts (PR) where even after submitting
+ the PR, the `status` remains "Draft". `per_billed` field was copied over from previous
+ doc (PO), hence it is recalculated for setting new correct status of PR.
+"""
+
+import frappe
+
+logger = frappe.logger("patch", allow_site=True, file_count=50)
+
+def execute():
+ affected_purchase_receipts = frappe.db.sql(
+ """select name from `tabPurchase Receipt`
+ where status = 'Draft' and per_billed = 100 and docstatus = 1"""
+ )
+
+ if not affected_purchase_receipts:
+ return
+
+ logger.info("purchase_receipt_status: begin patch, PR count: {}"
+ .format(len(affected_purchase_receipts)))
+
+
+ for pr in affected_purchase_receipts:
+ pr_name = pr[0]
+ logger.info("purchase_receipt_status: patching PR - {}".format(pr_name))
+
+ pr_doc = frappe.get_doc("Purchase Receipt", pr_name)
+
+ pr_doc.update_billing_status(update_modified=False)
+ pr_doc.set_status(update=True, update_modified=False)
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 83%
rename from erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
rename to erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
index c26cddb..01a4ae0 100644
--- a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
+++ b/erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
@@ -1,6 +1,7 @@
import frappe
def execute():
+ frappe.reload_doc('custom', 'doctype', 'custom_field')
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py
index 9ad48e2..c92d52d 100644
--- a/erpnext/patches/v13_0/check_is_income_tax_component.py
+++ b/erpnext/patches/v13_0/check_is_income_tax_component.py
@@ -8,36 +8,39 @@
def execute():
- doctypes = ['salary_component',
- 'Employee Tax Exemption Declaration',
- 'Employee Tax Exemption Proof Submission',
- 'Employee Tax Exemption Declaration Category',
- 'Employee Tax Exemption Proof Submission Detail'
- ]
+ doctypes = ['salary_component',
+ 'Employee Tax Exemption Declaration',
+ 'Employee Tax Exemption Proof Submission',
+ 'Employee Tax Exemption Declaration Category',
+ 'Employee Tax Exemption Proof Submission Detail',
+ 'gratuity_rule',
+ 'gratuity_rule_slab',
+ 'gratuity_applicable_component'
+ ]
- for doctype in doctypes:
- frappe.reload_doc('Payroll', 'doctype', doctype)
+ for doctype in doctypes:
+ frappe.reload_doc('Payroll', 'doctype', doctype)
- reports = ['Professional Tax Deductions', 'Provident Fund Deductions']
- for report in reports:
- frappe.reload_doc('Regional', 'Report', report)
- frappe.reload_doc('Regional', 'Report', report)
+ reports = ['Professional Tax Deductions', 'Provident Fund Deductions']
+ for report in reports:
+ frappe.reload_doc('Regional', 'Report', report)
+ frappe.reload_doc('Regional', 'Report', report)
- if erpnext.get_region() == "India":
- setup(patch=True)
+ if erpnext.get_region() == "India":
+ setup(patch=True)
- if frappe.db.exists("Salary Component", "Income Tax"):
- frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
- if frappe.db.exists("Salary Component", "TDS"):
- frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1)
+ if frappe.db.exists("Salary Component", "Income Tax"):
+ frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
+ if frappe.db.exists("Salary Component", "TDS"):
+ frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1)
- components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1)
- for component in components:
- frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1)
+ components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1)
+ for component in components:
+ frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1)
- if erpnext.get_region() == "India":
- if frappe.db.exists("Salary Component", "Provident Fund"):
- frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund")
- if frappe.db.exists("Salary Component", "Professional Tax"):
- frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax")
\ No newline at end of file
+ if erpnext.get_region() == "India":
+ if frappe.db.exists("Salary Component", "Provident Fund"):
+ frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund")
+ if frappe.db.exists("Salary Component", "Professional Tax"):
+ frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
index 48d5cb4..59b2e49 100644
--- a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
+++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
@@ -11,4 +11,8 @@
if not company:
return
+
+ frappe.reload_doc('accounts', 'doctype', 'pos_invoice')
+ frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item')
+
make_custom_fields()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/fix_non_unique_represents_company.py b/erpnext/patches/v13_0/fix_non_unique_represents_company.py
new file mode 100644
index 0000000..61dc824
--- /dev/null
+++ b/erpnext/patches/v13_0/fix_non_unique_represents_company.py
@@ -0,0 +1,8 @@
+import frappe
+
+def execute():
+ frappe.db.sql("""
+ update tabCustomer
+ set represents_company = NULL
+ where represents_company = ''
+ """)
\ No newline at end of file
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..d927524 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", "Inpatient Medication Order")
+ frappe.reload_doc("healthcare", "doctype", "Therapy Session")
+ frappe.reload_doc("healthcare", "doctype", "Clinical Procedure")
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py
index d7a5c68..1830bab 100644
--- a/erpnext/patches/v13_0/setup_uae_vat_fields.py
+++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py
@@ -2,11 +2,15 @@
# License: GNU General Public License v3. See license.txt
import frappe
-from erpnext.regional.united_arab_emirates.setup import setup
+from erpnext.regional.united_arab_emirates.setup import setup
def execute():
company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'})
if not company:
return
- setup()
\ No newline at end of file
+ frappe.reload_doc('regional', 'report', 'uae_vat_201')
+ frappe.reload_doc('regional', 'doctype', 'uae_vat_settings')
+ frappe.reload_doc('regional', 'doctype', 'uae_vat_account')
+
+ setup()
diff --git a/erpnext/patches/v13_0/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py
new file mode 100644
index 0000000..4816b40
--- /dev/null
+++ b/erpnext/patches/v13_0/update_payment_terms_outstanding.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "Payment Schedule")
+ if frappe.db.count('Payment Schedule'):
+ frappe.db.sql('''
+ UPDATE
+ `tabPayment Schedule` ps
+ SET
+ ps.outstanding = (ps.payment_amount - ps.paid_amount)
+ ''')
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/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json
index 2b29f66..5e17a5c 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.json
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json
@@ -163,7 +163,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -176,7 +175,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:51:13.419716",
+ "modified": "2021-03-31 22:33:59.098532",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Additional Salary",
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
index 4c45580..8332697 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
@@ -124,7 +124,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -148,7 +147,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-14 15:52:08.566418",
+ "modified": "2021-03-31 22:35:08.940087",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
index ea9ccd5..e1f8431 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
@@ -21,7 +21,6 @@
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
- frm.set_df_property('currency', 'hidden', 0);
}
}
});
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
index da24aac..b3bac01 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
@@ -125,10 +125,9 @@
"label": "Attachments"
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
- "hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1,
@@ -145,7 +144,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:49:56.097352",
+ "modified": "2021-03-31 22:37:21.024625",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Claim",
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
index e5b1052..0d10b2c 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
@@ -75,7 +75,6 @@
"reqd": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -95,7 +94,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:22:16.468042",
+ "modified": "2021-03-31 22:38:20.332316",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Incentive",
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
index 0e0c9b5..fb11875 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
@@ -47,5 +47,26 @@
});
}).addClass("btn-primary");
}
+ },
+
+ employee: function(frm) {
+ if (frm.doc.employee) {
+ frm.trigger('get_employee_currency');
+ }
+ },
+
+ get_employee_currency: function(frm) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
+ args: {
+ employee: frm.doc.employee,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value('currency', r.message);
+ frm.refresh_fields();
+ }
+ }
+ });
}
});
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
index 83d4ae5..b247d26 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
@@ -108,7 +108,7 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -119,7 +119,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 16:42:24.493761",
+ "modified": "2021-03-31 22:39:59.237361",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Declaration",
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
index 497f35c..4fb0a37 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
@@ -58,5 +58,26 @@
currency: function(frm) {
frm.refresh_fields();
- }
+ },
+
+ employee: function(frm) {
+ if (frm.doc.employee) {
+ frm.trigger('get_employee_currency');
+ }
+ },
+
+ get_employee_currency: function(frm) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
+ args: {
+ employee: frm.doc.employee,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value('currency', r.message);
+ frm.refresh_fields();
+ }
+ }
+ });
+ },
});
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
index 53f18cb..77b107e 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
@@ -131,7 +131,7 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -142,7 +142,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 16:47:03.410020",
+ "modified": "2021-03-31 22:41:13.723339",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Proof Submission",
diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
index 9fa261d..5a7de37 100644
--- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
+++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
@@ -93,7 +93,7 @@
"options": "Income Tax Slab Other Charges"
},
{
- "default": "Company:company:default_currency",
+ "fetch_from": "company.default_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -104,7 +104,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-19 13:54:24.728075",
+ "modified": "2021-03-31 22:42:08.139520",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Income Tax Slab",
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 6bcd4e0..4c9469e 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 "
@@ -95,6 +96,7 @@
return emp_list
+ @frappe.whitelist()
def fill_employee_details(self):
self.set('employees', [])
employees = self.get_emp_list()
@@ -142,6 +144,7 @@
if not self.get(fieldname):
frappe.throw(_("Please set {0}").format(self.meta.get_label(fieldname)))
+ @frappe.whitelist()
def create_salary_slips(self):
"""
Creates salary slip for selected employees if already not created
@@ -174,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)
@@ -268,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)
@@ -329,13 +332,13 @@
amount = flt(amount) * flt(conversion_rate)
return exchange_rate, amount
+ @frappe.whitelist()
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
@@ -367,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
@@ -406,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
@@ -421,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):
@@ -438,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
@@ -550,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({
@@ -562,34 +567,26 @@
if publish_progress:
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()
+ else:
+ 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 = []
@@ -641,3 +638,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/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
index 6647230..7ea6210 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
@@ -93,7 +93,6 @@
"reqd": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -106,7 +105,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:27:47.003134",
+ "modified": "2021-03-31 22:43:28.363644",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Retention Bonus",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index d527839..a0ddd39 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,27 +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", "" );
+ }
}
}
},
@@ -216,7 +221,7 @@
});
var set_totals = function(frm) {
- if (frm.doc.docstatus === 0) {
+ if (frm.doc.docstatus === 0 && frm.doc.doctype === "Salary Slip") {
if (frm.doc.earnings || frm.doc.deductions) {
frappe.call({
method: "set_totals",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 6688368..42a0f29 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -500,7 +500,6 @@
"fieldtype": "Column Break"
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)",
"fetch_from": "salary_structure.currency",
"fieldname": "currency",
@@ -632,7 +631,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-02-19 11:48:05.383945",
+ "modified": "2021-03-31 22:44:09.772331",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index a04a635..afdf081 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 = ''
@@ -142,6 +145,7 @@
self.start_date = date_details.start_date
self.end_date = date_details.end_date
+ @frappe.whitelist()
def get_emp_and_working_day_details(self):
'''First time, load all the components from salary structure'''
if self.employee:
@@ -594,10 +598,10 @@
continue
if (
- not d.additional_salary
- and (not additional_salary or additional_salary.overwrite)
- or additional_salary
- and additional_salary.name == d.additional_salary
+ (not d.additional_salary
+ and (not additional_salary or additional_salary.overwrite))
+ or (additional_salary
+ and additional_salary.name == d.additional_salary)
):
component_row = d
break
@@ -607,7 +611,7 @@
self.set(component_type, [
d for d in self.get(component_type)
if d.salary_component != component_data.salary_component
- or d.additional_salary and additional_salary.name != d.additional_salary
+ or (d.additional_salary and additional_salary.name != d.additional_salary)
or d == component_row
])
@@ -617,13 +621,16 @@
component_row = self.append(component_type)
for attr in (
- 'depends_on_payment_days', 'salary_component', 'abbr'
+ 'depends_on_payment_days', 'salary_component',
'do_not_include_in_total', 'is_tax_applicable',
'is_flexible_benefit', 'variable_based_on_taxable_salary',
'exempted_from_income_tax'
):
component_row.set(attr, component_data.get(attr))
+ abbr = component_data.get('abbr') or component_data.get('salary_component_abbr')
+ component_row.set('abbr', abbr)
+
if additional_salary:
component_row.default_amount = 0
component_row.additional_amount = amount
@@ -1049,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:
@@ -1114,10 +1121,12 @@
self.bank_name = emp.bank_name
self.bank_account_no = emp.bank_ac_no
+ @frappe.whitelist()
def process_salary_based_on_working_days(self):
self.get_working_days_details(lwp=self.leave_without_pay)
self.calculate_net_pay()
+ @frappe.whitelist()
def set_totals(self):
self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1:
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 7672695..01e4170 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -312,7 +312,7 @@
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
- payroll_period, deduct_random=False)
+ payroll_period, deduct_random=False, num=6)
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
'test_ytd@salary.com'}, order_by = 'posting_date')
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/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json
index de56fc8..5dd1d70 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.json
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json
@@ -232,7 +232,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-30 11:30:32.190798",
+ "modified": "2021-03-31 15:41:12.342380",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure",
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index 1712081..352c180 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -100,7 +100,7 @@
from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
assign_salary_structure_for_employees(employees, self,
- payroll_payable_account=payroll_payable_account,
+ payroll_payable_account=payroll_payable_account,
from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
frappe.msgprint(_("No Employee Found"))
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index f2fb558..36387f2 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -164,7 +164,13 @@
salary_structure_assignment.employee = employee
salary_structure_assignment.base = 50000
salary_structure_assignment.variable = 5000
- salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1)
+
+ if getdate(nowdate()).day == 1:
+ date = from_date or nowdate()
+ else:
+ date = from_date or add_days(nowdate(), -1)
+
+ salary_structure_assignment.from_date = date
salary_structure_assignment.salary_structure = salary_structure
salary_structure_assignment.currency = currency
salary_structure_assignment.payroll_payable_account = get_payable_account(company)
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index 92bb347..c8b98e5 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -125,7 +125,6 @@
"options": "Income Tax Slab"
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)",
"fetch_from": "salary_structure.currency",
"fieldname": "currency",
@@ -146,7 +145,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-30 18:07:48.251311",
+ "modified": "2021-03-31 22:44:46.267974",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
diff --git a/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json b/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json
index 4bae675..b1a7cc2 100644
--- a/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json
+++ b/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json
@@ -8,7 +8,7 @@
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-06-01 11:53:54.553947",
+ "modified": "2020-06-29 11:53:54.553947",
"modified_by": "Administrator",
"name": "Create Payroll Period",
"owner": "Administrator",
diff --git a/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json b/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json
index 946b8c8..a7cf7bf 100644
--- a/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json
+++ b/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json
@@ -1,19 +1,19 @@
{
- "action": "Go to Page",
+ "action": "Update Settings",
"creation": "2020-06-04 16:34:29.664917",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
- "is_single": 0,
+ "is_single": 1,
"is_skipped": 0,
- "modified": "2020-06-04 16:34:29.664917",
+ "modified": "2020-06-29 16:34:29.664917",
"modified_by": "Administrator",
"name": "Payroll Settings",
"owner": "Administrator",
- "path": "#Form/Payroll Settings",
+ "reference_document": "Payroll Settings",
"show_full_form": 0,
"title": "Payroll Settings",
- "validate_action": 1
+ "validate_action": 0
}
\ No newline at end of file
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/project.js b/erpnext/projects/doctype/project/project.js
index 077011a..c5265e2 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -18,8 +18,8 @@
};
},
onload: function (frm) {
- var so = frappe.meta.get_docfield("Project", "sales_order");
- so.get_route_options_for_new_doc = function (field) {
+ const so = frm.get_docfield("sales_order");
+ so.get_route_options_for_new_doc = () => {
if (frm.is_new()) return;
return {
"customer": frm.doc.customer,
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 15a2873..70139c6 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -4,12 +4,14 @@
import frappe, unittest
-test_records = frappe.get_test_records('Project')
-test_ignore = ["Sales Order"]
+from frappe.utils import getdate, nowdate, add_days
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.task.test_task import create_task
-from frappe.utils import getdate, nowdate, add_days
+
+test_records = frappe.get_test_records('Project')
+test_ignore = ["Sales Order"]
+
class TestProject(unittest.TestCase):
def test_project_with_template_having_no_parent_and_depend_tasks(self):
@@ -31,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:
@@ -51,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))
@@ -112,7 +118,8 @@
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
- expected_start_date = args.start_date
+ expected_start_date = args.start_date,
+ company= args.company or '_Test Company'
))
if args.project_template_name:
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/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 3a3ee38..2e133be 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -323,12 +323,15 @@
// set precision in the last item iteration
if (n == me.frm.doc["items"].length - 1) {
me.round_off_totals(tax);
+ me.set_in_company_currency(tax,
+ ["tax_amount", "tax_amount_after_discount_amount"]);
+
+ me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each item
me.set_cumulative_total(i, tax);
- me.set_in_company_currency(tax,
- ["total", "tax_amount", "tax_amount_after_discount_amount"]);
+ me.set_in_company_currency(tax, ["total"]);
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
@@ -393,20 +396,11 @@
current_tax_amount = tax_rate * item.qty;
}
- current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
- get_final_tax_amount: function(tax, current_tax_amount) {
- if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
- current_tax_amount = Math.round(current_tax_amount);
- }
-
- return current_tax_amount;
- },
-
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;
@@ -420,10 +414,22 @@
},
round_off_totals: function(tax) {
+ if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
+ tax.tax_amount= Math.round(tax.tax_amount);
+ tax.tax_amount_after_discount_amount = Math.round(tax.tax_amount_after_discount_amount);
+ }
+
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, precision("tax_amount", tax));
},
+ round_off_base_values: function(tax) {
+ if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
+ tax.base_tax_amount= Math.round(tax.base_tax_amount);
+ tax.base_tax_amount_after_discount_amount = Math.round(tax.base_tax_amount_after_discount_amount);
+ }
+ },
+
manipulate_grand_total_for_inclusive_tax: function() {
var me = this;
// if fully inclusive taxes and diff
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 32d371d..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);
},
@@ -1167,6 +1173,11 @@
this.calculate_net_weight();
}
+ // for handling customization not to fetch price list rate
+ if(frappe.flags.dont_fetch_price_list_rate) {
+ return
+ }
+
if (!dont_fetch_price_list_rate &&
frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true);
diff --git a/erpnext/public/js/website_theme.js b/erpnext/public/js/website_theme.js
new file mode 100644
index 0000000..0009cac
--- /dev/null
+++ b/erpnext/public/js/website_theme.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See license.txt
+
+frappe.ui.form.on('Website Theme', {
+ validate(frm) {
+ let theme_scss = frm.doc.theme_scss;
+ if (theme_scss && theme_scss.includes('frappe/public/scss/website')
+ && !theme_scss.includes('erpnext/public/scss/website')
+ ) {
+ frm.set_value('theme_scss',
+ `${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`);
+ }
+ }
+});
diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
index bf82cc0..5a8ec73 100644
--- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
+++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class QualityFeedback(Document):
+ @frappe.whitelist()
def set_parameters(self):
if self.template and not getattr(self, 'parameters', []):
for d in frappe.get_doc('Quality Feedback Template', self.template).parameters:
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/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
index 888b2da..369a400 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
@@ -109,7 +109,7 @@
</td>
</tr>
<tr>
- <td>{{__("Suppliies made to Composition Taxable Persons")}}</td>
+ <td>{{__("Supplies made to Composition Taxable Persons")}}</td>
<td class="right">
{% for row in data.inter_sup.comp_details %}
{% if row %}
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index a49996d..a5dd5a2 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -172,7 +172,6 @@
self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt):
-
self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2)
self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2)
self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2)
@@ -238,7 +237,6 @@
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
def set_inter_state_supply(self, inter_state_supply):
-
osup_det = self.report_dict["sup_details"]["osup_det"]
for key, value in iteritems(inter_state_supply):
@@ -352,10 +350,18 @@
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
+ and p.gst_category != 'Registered Composition'
and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply
+ FROM `tabPurchase Invoice`
+ WHERE docstatus = 1 and gst_category = 'Registered Composition'
+ and month(posting_date) = %s and year(posting_date) = %s
+ and company = %s and company_gstin = %s
+ group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+
inward_nil_exempt_details = {
"gst": {
"intra": 0.0,
@@ -369,9 +375,11 @@
for d in inward_nil_exempt:
if d.place_of_supply:
- if d.is_nil_exempt == 1 and state == d.place_of_supply.split("-")[1]:
+ if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
- elif d.is_nil_exempt == 1 and state != d.place_of_supply.split("-")[1]:
+ elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state != d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 023b4ed..ef8af24 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -64,7 +64,7 @@
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
- self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250)
+ self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
@@ -228,6 +228,19 @@
pi1.submit()
+ pi2 = make_purchase_invoice(company="_Test Company GST",
+ customer = '_Test Registered Supplier',
+ currency = 'INR',
+ item = 'Milk',
+ warehouse = 'Finished Goods - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=250,
+ qty=1,
+ do_not_save=1
+ )
+ pi2.submit()
+
def make_suppliers():
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
index 5bbd575..41a0f11 100644
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -50,6 +50,7 @@
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
get_link_to_form('Company', self.company)))
+ @frappe.whitelist()
def set_company_address(self):
address = get_company_address(self.company)
self.company_address = address.company_address
@@ -70,6 +71,7 @@
else:
self.title = self.donor_name
+ @frappe.whitelist()
def get_payments(self):
if not self.member:
frappe.throw(_('Please select a Member first.'))
@@ -81,7 +83,7 @@
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
- }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
+ }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
if not memberships:
frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py
index d6047e8..ac1f543 100644
--- a/erpnext/regional/germany/setup.py
+++ b/erpnext/regional/germany/setup.py
@@ -3,4 +3,17 @@
def setup(company=None, patch=True):
- pass
+ add_custom_roles_for_reports()
+
+
+def add_custom_roles_for_reports():
+ """Add Access Control to UAE VAT 201."""
+ if not frappe.db.get_value('Custom Role', dict(report='DATEV')):
+ frappe.get_doc(dict(
+ doctype='Custom Role',
+ report='DATEV',
+ roles= [
+ dict(role='Accounts User'),
+ dict(role='Accounts Manager')
+ ]
+ )).insert()
\ No newline at end of file
diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py
index 378b735..faeb36f 100644
--- a/erpnext/regional/india/__init__.py
+++ b/erpnext/regional/india/__init__.py
@@ -69,7 +69,7 @@
"Mizoram": "15",
"Nagaland": "13",
"Odisha": "21",
- "Other Territory": "98",
+ "Other Territory": "97",
"Pondicherry": "34",
"Punjab": "03",
"Rajasthan": "08",
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
index 86290cf..f4a3542 100644
--- a/erpnext/regional/india/e_invoice/einv_validation.json
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -919,7 +919,8 @@
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
- "description": "Tranport Document Number"
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
},
"TransDocDt": {
"type": "string",
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 7cd64f2..8d682be 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;
@@ -45,7 +46,7 @@
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
- {
+ {
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
@@ -60,7 +61,7 @@
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
- args: {
+ args: {
doctype,
docname: name,
irn: irn,
@@ -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);
}
@@ -254,7 +235,7 @@
title: __("Preview"),
size: "large",
fields: [
- {
+ {
"label": "Preview",
"fieldname": "preview_html",
"fieldtype": "HTML"
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 96f7f1b..605f4e1 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,53 +117,38 @@
invoice_date=invoice_date
))
-def get_party_details(address_name):
- d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
-
- if (not d.gstin
- 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:
- # according to einvoice standard
- pincode = 999999
+def get_party_details(address_name, is_shipping_address=False):
+ addr = frappe.get_doc('Address', address_name)
+
+ validate_address_fields(addr, is_shipping_address)
- return frappe._dict(dict(
- gstin=d.gstin,
- 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)
+ if addr.gst_state_number == 97:
+ # according to einvoice standard
+ addr.pincode = 999999
+
+ party_address_details = frappe._dict(dict(
+ 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)
))
-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)
+ return party_address_details
def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city = frappe.db.get_value(
@@ -169,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
@@ -205,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]
@@ -223,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
@@ -230,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
@@ -254,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
@@ -262,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])
@@ -280,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')
@@ -287,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' }
@@ -305,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.'),
@@ -319,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)
@@ -334,24 +426,30 @@
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
- place_of_supply = get_place_of_supply(invoice, invoice.doctype) or sanitize_for_json(invoice.billing_address_gstin)
- place_of_supply = place_of_supply[:2]
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype)
+ if place_of_supply:
+ place_of_supply = place_of_supply.split('-')[0]
+ else:
+ 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_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
@@ -364,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
@@ -391,70 +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=[]):
- 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'
@@ -463,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')
@@ -522,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):
@@ -544,16 +657,15 @@
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
def get_gstin_details(gstin):
'''fetch and cache GSTIN details'''
@@ -569,12 +681,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'))
@@ -594,12 +707,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()
@@ -616,21 +753,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,
@@ -643,12 +789,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)
@@ -687,7 +862,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):
@@ -719,7 +894,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):
@@ -734,6 +909,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
@@ -745,22 +923,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:
@@ -780,8 +942,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()
@@ -791,7 +958,6 @@
'label': _('IRN Generated')
}
self.update_invoice()
-
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
@@ -818,6 +984,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."""
@@ -847,5 +1024,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 ee49aae..9ded8da 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -5,19 +5,21 @@
import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today
def setup(company=None, patch=True):
- setup_company_independent_fixtures()
+ setup_company_independent_fixtures(patch=patch)
if not patch:
make_fixtures(company)
# TODO: for all countries
-def setup_company_independent_fixtures():
+def setup_company_independent_fixtures(patch=False):
make_custom_fields()
+ make_property_setters(patch=patch)
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
@@ -49,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(
@@ -110,6 +112,12 @@
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
+def make_property_setters(patch=False):
+ # GST rules do not allow for an invoice no. bigger than 16 characters
+ if not patch:
+ make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
+ make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
+
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
@@ -120,6 +128,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',
@@ -149,6 +160,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,
@@ -273,7 +291,7 @@
'allow_on_submit': 1,
'insert_after': 'customer_name_in_arabic',
'translatable': 0,
- }
+ }
]
si_ewaybill_fields = [
@@ -401,21 +419,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 = {
@@ -431,7 +465,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': [
@@ -446,7 +480,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],
@@ -860,4 +894,4 @@
})
rule.flags.ignore_mandatory = True
- rule.save()
\ No newline at end of file
+ rule.save()
diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py
index 7ce27f6..a16f56c 100644
--- a/erpnext/regional/india/test_utils.py
+++ b/erpnext/regional/india/test_utils.py
@@ -12,14 +12,14 @@
mock_get_cached.return_value = "India" # mock country
posting_date = "2021-05-01"
- invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
- "SI.2020.0001", "PI2021 - 001" ]
+ invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05",
+ "SI.2020.0001", "PI2021 - 001"]
for name in invalid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
self.assertRaises(frappe.ValidationError, validate_document_name, doc)
- valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
- "2020-PI-0001", "PI2020-0001" ]
+ valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001",
+ "2020-PI-0001", "PI2020-0001"]
for name in valid_names:
doc = frappe._dict(name=name, posting_date=posting_date)
try:
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 3637de4..6338056 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:
@@ -154,6 +155,7 @@
def validate_document_name(doc, method=None):
"""Validate GST invoice number requirements."""
+
country = frappe.get_cached_value("Company", doc.company, "country")
# Date was chosen as start of next FY to avoid irritating current users.
@@ -832,3 +834,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/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json
index 80a866c..94e3960 100644
--- a/erpnext/regional/report/datev/datev.json
+++ b/erpnext/regional/report/datev/datev.json
@@ -1,29 +1,22 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 0,
- "creation": "2019-04-24 08:45:16.650129",
- "disabled": 0,
- "icon": "octicon octicon-repo-pull",
- "color": "#4CB944",
- "docstatus": 0,
- "doctype": "Report",
- "idx": 0,
- "is_standard": "Yes",
- "module": "Regional",
- "name": "DATEV",
- "owner": "Administrator",
- "ref_doctype": "GL Entry",
- "report_name": "DATEV",
- "report_type": "Script Report",
- "roles": [
- {
- "role": "Accounts User"
- },
- {
- "role": "Accounts Manager"
- },
- {
- "role": "Auditor"
- }
- ]
-}
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2019-04-24 08:45:16.650129",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-04-06 12:23:00.379517",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "DATEV",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "DATEV",
+ "report_type": "Script Report",
+ "roles": []
+}
\ No newline at end of file
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/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 62faa30..7507623 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -199,7 +199,7 @@
self.item_tax_rate = frappe._dict()
items = frappe.db.sql("""
- select item_code, parent, base_net_amount, item_tax_rate
+ select item_code, parent, taxable_value, item_tax_rate
from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -207,7 +207,7 @@
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
- sum(i.get('base_net_amount', 0) for i in items
+ sum(i.get('taxable_value', 0) for i in items
if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {}
diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py
index f899349..616c2b8 100644
--- a/erpnext/regional/report/gstr_2/gstr_2.py
+++ b/erpnext/regional/report/gstr_2/gstr_2.py
@@ -44,7 +44,7 @@
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items():
- if rate:
+ if rate or invoice_details.get('gst_category') == 'Registered Composition':
if inv not in self.igst_invoices:
rate = rate / 2
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
@@ -86,7 +86,7 @@
conditions += opts[1]
if self.filters.get("type_of_business") == "B2B":
- conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1 "
+ conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 "
elif self.filters.get("type_of_business") == "CDNR":
conditions += """ and is_return = 1 """
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 7d5e84d..cd94ee1 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -212,7 +212,8 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
- "options": "Company"
+ "options": "Company",
+ "unique": 1
},
{
"depends_on": "represents_company",
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index c452594..96b3fa4 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -230,13 +230,20 @@
frappe.db.set(self, "customer_name", newdn)
def set_loyalty_program(self):
- if self.loyalty_program: return
+ if self.loyalty_program:
+ return
+
loyalty_program = get_loyalty_programs(self)
- if not loyalty_program: return
+ if not loyalty_program:
+ return
+
if len(loyalty_program) == 1:
self.loyalty_program = loyalty_program[0]
else:
- frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually."))
+ frappe.msgprint(
+ _("Multiple Loyalty Programs found for Customer {}. Please select manually.")
+ .format(frappe.bold(self.customer_name))
+ )
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
@@ -340,7 +347,6 @@
@frappe.whitelist()
def get_loyalty_programs(doc):
''' returns applicable loyalty programs for a customer '''
- from frappe.desk.treeview import get_children
lp_details = []
loyalty_programs = frappe.get_all("Loyalty Program",
@@ -349,15 +355,33 @@
"ifnull(to_date, '2500-01-01')": [">=", today()]})
for loyalty_program in loyalty_programs:
- customer_groups = [d.value for d in get_children("Customer Group", loyalty_program.customer_group)] + [loyalty_program.customer_group]
- customer_territories = [d.value for d in get_children("Territory", loyalty_program.customer_territory)] + [loyalty_program.customer_territory]
-
- if (not loyalty_program.customer_group or doc.customer_group in customer_groups)\
- and (not loyalty_program.customer_territory or doc.territory in customer_territories):
+ if (
+ (not loyalty_program.customer_group
+ or doc.customer_group in get_nested_links(
+ "Customer Group",
+ loyalty_program.customer_group,
+ doc.flags.ignore_permissions
+ ))
+ and (not loyalty_program.customer_territory
+ or doc.territory in get_nested_links(
+ "Territory",
+ loyalty_program.customer_territory,
+ doc.flags.ignore_permissions
+ ))
+ ):
lp_details.append(loyalty_program.name)
return lp_details
+def get_nested_links(link_doctype, link_name, ignore_permissions=False):
+ from frappe.desk.treeview import _get_children
+
+ links = [link_name]
+ for d in _get_children(link_doctype, link_name, ignore_permissions):
+ links.append(d.value)
+
+ return links
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
@@ -572,4 +596,4 @@
""", {
'customer': customer,
'txt': '%%%s%%' % txt
- })
\ 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/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 5da248c..246f923 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -64,6 +64,7 @@
opp = frappe.get_doc("Opportunity", opportunity)
opp.set_status(status=status, update=True)
+ @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index e561291..d9e52e1 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -150,7 +150,7 @@
if enq:
frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0]))
- def update_prevdoc_status(self, flag):
+ def update_prevdoc_status(self, flag=None):
for quotation in list(set([d.prevdoc_docname for d in self.get("items")])):
if quotation:
doc = frappe.get_doc("Quotation", quotation)
@@ -372,6 +372,7 @@
self.indicator_color = "green"
self.indicator_title = _("Paid")
+ @frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
'''Returns items with BOM that already do not have a linked work order'''
items = []
@@ -778,6 +779,7 @@
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
+ """Creates Purchase Order for each Supplier. Returns a list of doc objects."""
if not selected_items: return
if isinstance(selected_items, string_types):
@@ -820,15 +822,16 @@
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
- suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
- suppliers = list(set(suppliers))
+ suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')]
+ suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order
- items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
+ items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')]
items_to_map = list(set(items_to_map))
if not suppliers:
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
+ purchase_orders = []
for supplier in suppliers:
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -872,7 +875,9 @@
doc.insert()
frappe.db.commit()
- return doc
+ purchase_orders.append(doc)
+
+ return purchase_orders
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 0fdfb1b..3137621 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -341,6 +341,9 @@
prev_total = so.get("base_total")
prev_total_in_words = so.get("base_in_words")
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
first_item_of_so = so.get("items")[0]
trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@@ -354,6 +357,10 @@
self.assertEqual(so.get("items")[-1].rate, 200)
self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400)
+
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7)
+
self.assertEqual(so.status, 'To Deliver and Bill')
updated_total = so.get("base_total")
@@ -373,6 +380,9 @@
create_dn_against_so(so.name, 2)
make_sales_invoice(so.name)
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
# add an item so as to try removing items
trans_item = json.dumps([
{"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
@@ -382,6 +392,9 @@
so.reload()
self.assertEqual(len(so.get("items")), 2)
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2)
+
# check if delivered items can be removed
trans_item = json.dumps([{
"item_code": '_Test Item 2',
@@ -402,6 +415,10 @@
so.reload()
self.assertEqual(len(so.get("items")), 1)
+
+ # reserved qty should decrease (back to initial) after deleting row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item)
+
self.assertEqual(so.status, 'To Deliver and Bill')
@@ -503,12 +520,18 @@
so = make_sales_order(item_code = "_Test Item", warehouse=None)
+ # get reserved qty of packed item
+ existing_reserved_qty = get_reserved_qty("_Packed Item")
+
added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}])
update_child_qty_rate('Sales Order', added_item, so.name)
so.reload()
self.assertEqual(so.packed_items[0].qty, 4)
+ # reserved qty in packed item should increase after adding bundle item
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4)
+
# test uom and conversion factor change
update_uom_conv_factor = json.dumps([{
'item_code': so.get("items")[0].item_code,
@@ -523,6 +546,9 @@
so.reload()
self.assertEqual(so.packed_items[0].qty, 8)
+ # reserved qty in packed item should increase after changing bundle item uom
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 8)
+
def test_update_child_with_tax_template(self):
"""
Test Action: Create a SO with one item having its tax account head already in the SO.
@@ -736,7 +762,7 @@
so = make_sales_order(item_list=so_items, do_not_submit=True)
so.submit()
- po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po.submit()
dn = create_dn_against_so(so.name, delivered_qty=2)
@@ -818,7 +844,7 @@
so.submit()
# create po for only one item
- po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po1.submit()
self.assertEqual(so.customer, po1.customer)
@@ -828,7 +854,7 @@
self.assertEqual(len(po1.items), 1)
# create po for remaining item
- po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])[0]
po2.submit()
# teardown
@@ -839,6 +865,45 @@
so.load_from_db()
so.cancel()
+ def test_drop_shipping_full_for_default_suppliers(self):
+ """Test if multiple POs are generated in one go against different default suppliers."""
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 1"):
+ make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 2"):
+ make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": "_Test Item for Drop Shipping 1",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Item for Drop Shipping 2",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier 1'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)
+
+ self.assertEqual(len(purchase_orders), 2)
+ self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
+ self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 2104c01..f01934b 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -18,6 +18,8 @@
"dn_required",
"sales_update_frequency",
"maintain_same_sales_rate",
+ "maintain_same_rate_action",
+ "role_to_override_stop_action",
"editable_price_list_rate",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
@@ -133,6 +135,23 @@
"fieldname": "hide_tax_id",
"fieldtype": "Check",
"label": "Hide Customer's Tax ID from Sales Transactions"
+ },
+ {
+ "default": "Stop",
+ "depends_on": "maintain_same_sales_rate",
+ "description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
+ "fieldname": "maintain_same_rate_action",
+ "fieldtype": "Select",
+ "label": "Action If Same Rate is Not Maintained",
+ "mandatory_depends_on": "maintain_same_sales_rate",
+ "options": "Stop\nWarn"
+ },
+ {
+ "depends_on": "eval: doc.maintain_same_rate_action == 'Stop'",
+ "fieldname": "role_to_override_stop_action",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Override Stop Action",
+ "options": "Role"
}
],
"icon": "fa fa-cog",
@@ -140,7 +159,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-03-02 17:35:53.603607",
+ "modified": "2021-04-04 20:18:12.814624",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
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/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 9ab9eef..11a63b3 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -7,7 +7,6 @@
this.allowed_customer_groups = settings.customer_groups;
this.allow_rate_change = settings.allow_rate_change;
this.allow_discount_change = settings.allow_discount_change;
-
this.init_component();
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index cb0a010..32a4556 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -201,7 +201,6 @@
me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
const item_row = frappe.get_doc(me.doctype, me.name);
const doc = me.events.get_frm().doc;
-
me.$item_price.html(format_currency(item_row.rate, doc.currency));
me.render_discount_dom(item_row);
});
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index b10a9e3..a5a739c 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -176,6 +176,14 @@
this.show_summary_placeholder();
});
+ this.$summary_container.on('click', '.delete-btn', () => {
+ this.events.delete_order(this.doc.name);
+ this.show_summary_placeholder();
+ // this.toggle_component(false);
+ // this.$component.find('.no-summary-placeholder').removeClass('d-none');
+ // this.$summary_wrapper.addClass('d-none');
+ });
+
this.$summary_container.on('click', '.new-btn', () => {
this.events.new_order();
this.toggle_component(false);
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 22a279d..600f160 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -252,6 +252,41 @@
}
}
+ setup_listener_for_payments() {
+ frappe.realtime.on("process_phone_payment", (data) => {
+ const doc = this.events.get_frm().doc;
+ const { response, amount, success, failure_message } = data;
+ let message, title;
+
+ if (success) {
+ title = __("Payment Received");
+ if (amount >= doc.grand_total) {
+ frappe.dom.unfreeze();
+ message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
+ this.events.submit_invoice();
+ cur_frm.reload_doc();
+
+ } else {
+ message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]);
+ }
+ } else if (failure_message) {
+ message = failure_message;
+ title = __("Payment Failed");
+ }
+
+ frappe.msgprint({ "message": message, "title": title });
+ });
+ }
+
+ auto_set_remaining_amount() {
+ const doc = this.events.get_frm().doc;
+ const remaining_amount = doc.grand_total - doc.paid_amount;
+ const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
+ if (!current_value && remaining_amount > 0 && this.selected_mode) {
+ this.selected_mode.set_value(remaining_amount);
+ }
+ }
+
attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`);
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/company.js b/erpnext/setup/doctype/company/company.js
index c041d26..c2b5e4f 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -259,6 +259,7 @@
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
+ ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],
["exchange_gain_loss_account", {"root_type": "Expense"}],
@@ -275,7 +276,7 @@
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
- ["unrealized_profit_loss_account", {"root_type": "Liability"}]
+ ["unrealized_profit_loss_account", {"root_type": "Liability"},]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 56f60df..83cbf47 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -59,6 +59,7 @@
"default_deferred_expense_account",
"default_payroll_payable_account",
"default_expense_claim_payable_account",
+ "default_discount_account",
"section_break_22",
"cost_center",
"column_break_26",
@@ -733,6 +734,12 @@
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
+ },
+ {
+ "fieldname": "default_discount_account",
+ "fieldtype": "Link",
+ "label": "Default Payment Discount Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 433851c..0922171 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -66,6 +66,7 @@
if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)):
frappe.throw(_("Abbreviation already used for another company"))
+ @frappe.whitelist()
def create_default_tax_template(self):
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
create_sales_tax({
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/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index cbb4c7c..ac55fdf 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -24,6 +24,7 @@
self._accounts = {}
self.currency = frappe.db.get_value('Company', self.company, "default_currency")
+ @frappe.whitelist()
def get_users(self):
"""get list of users"""
user_list = frappe.db.sql("""
@@ -41,6 +42,7 @@
frappe.response['user_list'] = user_list
+ @frappe.whitelist()
def send(self):
# send email only to enabled users
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
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/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index fa7bc50..76a8450 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -50,6 +50,7 @@
# clear cache
frappe.clear_cache()
+ @frappe.whitelist()
def get_defaults(self):
return frappe.defaults.get_defaults()
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/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
index 2ea0bc0..c4f1de1 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ b/erpnext/setup/doctype/naming_series/naming_series.py
@@ -15,6 +15,7 @@
class NamingSeriesNotSetError(frappe.ValidationError): pass
class NamingSeries(Document):
+ @frappe.whitelist()
def get_transactions(self, arg=None):
doctypes = list(set(frappe.db.sql_list("""select parent
from `tabDocField` df where fieldname='naming_series'""")
@@ -53,6 +54,7 @@
options = list(filter(lambda x: x, [cstr(n).strip() for n in ol]))
return options
+ @frappe.whitelist()
def update_series(self, arg=None):
"""update series list"""
self.validate_series_set()
@@ -139,10 +141,12 @@
if not re.match("^[\w\- /.#{}]*$", n, re.UNICODE):
throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series'))
+ @frappe.whitelist()
def get_options(self, arg=None):
if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"):
return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options
+ @frappe.whitelist()
def get_current(self, arg=None):
"""get series current"""
if self.prefix:
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index 681d161..8515db3 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -112,9 +112,7 @@
def request_for_quotation():
quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True
- quotation.save()
- if not get_shopping_cart_settings().save_quotations_as_draft:
- quotation.submit()
+ quotation.submit()
return quotation.name
@frappe.whitelist()
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/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 04d624e..8e79f0e 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "MAT-BIN-.YYYY.-.#####",
"creation": "2013-01-10 16:34:25",
"doctype": "DocType",
@@ -112,7 +113,8 @@
{
"fieldname": "reserved_qty_for_sub_contract",
"fieldtype": "Float",
- "label": "Reserved Qty for sub contract"
+ "label": "Reserved Qty for sub contract",
+ "read_only": 1
},
{
"fieldname": "ma_rate",
@@ -166,7 +168,8 @@
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
- "modified": "2019-11-18 18:34:59.456882",
+ "links": [],
+ "modified": "2021-03-30 23:09:39.572776",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@@ -196,5 +199,6 @@
],
"quick_entry": 1,
"search_fields": "item_code,warehouse",
+ "sort_field": "modified",
"sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3544390..d326a04 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -101,7 +101,7 @@
for f in fieldname:
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
- super(DeliveryNote, self).before_print()
+ super(DeliveryNote, self).before_print(settings)
def set_actual_qty(self):
for d in self.get('items'):
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 28e9533..de85bc3 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -90,6 +90,7 @@
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes)))
+ @frappe.whitelist()
def process_route(self, optimize):
"""
Estimate the arrival times for each stop in the Delivery Trip.
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 7b7d2da..7cb84a6 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -50,6 +50,7 @@
self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series()
+ @frappe.whitelist()
def set_asset_naming_series(self):
if not hasattr(self, '_asset_naming_series'):
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
@@ -706,6 +707,7 @@
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
+ @frappe.whitelist()
def copy_specification_from_item_group(self):
self.set("website_specifications", [])
if self.item_group:
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 909c4ee..6cec852 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -12,6 +12,7 @@
"item_name": "_Test Item",
"apply_warehouse_wise_reorder_level": 1,
"gst_hsn_code": "999800",
+ "opening_stock": 10,
"valuation_rate": 100,
"item_defaults": [{
"company": "_Test Company",
@@ -58,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/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 69a8bf1..8310946 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -12,6 +12,7 @@
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
class LandedCostVoucher(Document):
+ @frappe.whitelist()
def get_items_from_purchase_receipts(self):
self.set("items", [])
for pr in self.get("purchase_receipts"):
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 527b0d3..7dfc5da 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -354,6 +354,10 @@
},
material_request_type: function(frm) {
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
+
+ if (frm.doc.material_request_type !== 'Material Transfer' && frm.doc.set_from_warehouse) {
+ frm.set_value('set_from_warehouse', '');
+ }
},
});
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index d73349d..8d7b238 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -20,9 +20,9 @@
"company",
"amended_from",
"warehouse_section",
- "set_warehouse",
- "column_break5",
"set_from_warehouse",
+ "column_break5",
+ "set_warehouse",
"items_section",
"scan_barcode",
"items",
@@ -314,7 +314,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-19 01:04:09.285862",
+ "modified": "2021-03-31 23:52:55.392512",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
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/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index a7a29cc..2008bff 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -152,6 +152,7 @@
return cint(recommended_case_no[0][0]) + 1
+ @frappe.whitelist()
def get_items(self):
self.set("items", [])
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index d723fac..6ab68e2 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -33,6 +33,7 @@
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ @frappe.whitelist()
def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@@ -345,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
@@ -378,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/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 70687bda..5d7597b 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -176,7 +176,7 @@
if flt(self.per_billed) < 100:
self.update_billing_status()
else:
- self.status = "Completed"
+ self.db_set("status", "Completed")
# Updating stock ledger should always be called after updating prevdoc status,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 7741ee7..7f0c3fa 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -191,7 +191,7 @@
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
-
+
pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
@@ -912,6 +912,57 @@
ste1.cancel()
po.cancel()
+
+ def test_po_to_pi_and_po_to_pr_worflow_full(self):
+ """Test following behaviour:
+ - Create PO
+ - Create PI from PO and submit
+ - Create PR from PO and submit
+ """
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
+ from erpnext.buying.doctype.purchase_order import purchase_order
+
+ po = test_purchase_order.create_purchase_order()
+
+ pi = purchase_order.make_purchase_invoice(po.name)
+ pi.submit()
+
+ pr = purchase_order.make_purchase_receipt(po.name)
+ pr.submit()
+
+ pr.load_from_db()
+
+ self.assertEqual(pr.status, "Completed")
+ self.assertEqual(pr.per_billed, 100)
+
+ def test_po_to_pi_and_po_to_pr_worflow_partial(self):
+ """Test following behaviour:
+ - Create PO
+ - Create partial PI from PO and submit
+ - Create PR from PO and submit
+ """
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
+ from erpnext.buying.doctype.purchase_order import purchase_order
+
+ po = test_purchase_order.create_purchase_order()
+
+ pi = purchase_order.make_purchase_invoice(po.name)
+ pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item.
+ pi.submit()
+
+ pr = purchase_order.make_purchase_receipt(po.name)
+ pr.save()
+ # per_billed is only updated after submission.
+ self.assertEqual(flt(pr.per_billed), 0)
+
+ pr.submit()
+
+ pi.load_from_db()
+ pr.load_from_db()
+
+ self.assertEqual(pr.status, "To Bill")
+ self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 58b1eca..469511a 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,7 @@
if self.readings:
self.inspect_and_set_status()
+ @frappe.whitelist()
def get_item_specification_details(self):
if not self.quality_inspection_template:
self.quality_inspection_template = frappe.db.get_value('Item',
@@ -32,6 +33,7 @@
child.update(d)
child.status = "Accepted"
+ @frappe.whitelist()
def get_quality_inspection_template(self):
template = ''
if self.bom_no:
@@ -62,17 +64,21 @@
(quality_inspection, self.modified, self.reference_name, self.item_code))
else:
+ args = [quality_inspection, self.modified, self.reference_name, self.item_code]
doctype = self.reference_type + ' Item'
+
if self.reference_type == 'Stock Entry':
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
conditions = ""
if self.batch_no and self.docstatus == 1:
- conditions += " and t1.batch_no = '%s'"%(self.batch_no)
+ conditions += " and t1.batch_no = %s"
+ args.append(self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
- conditions += " and t1.quality_inspection = '%s'"%(self.name)
+ conditions += " and t1.quality_inspection = %s"
+ args.append(self.name)
frappe.db.sql("""
UPDATE
@@ -85,7 +91,7 @@
and t1.parent = t2.name
{conditions}
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
- (quality_inspection, self.modified, self.reference_name, self.item_code))
+ args)
def inspect_and_set_status(self):
for reading in self.readings:
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 559f9a5..3f83780 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -39,6 +39,7 @@
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+ @frappe.whitelist()
def restart_reposting(self):
self.set_status('Queued')
frappe.enqueue(repost, timeout=1800, queue='long',
@@ -123,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 64dcbed..ef7d54a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -100,6 +100,13 @@
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
},
setup_quality_inspection: function(frm) {
@@ -551,7 +558,6 @@
})
);
}
-
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
@@ -721,7 +727,7 @@
no_batch_serial_number_value = !d.batch_no;
}
- if (no_batch_serial_number_value) {
+ if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
erpnext.stock.select_batch_and_serial_no(frm, d);
}
}
@@ -849,7 +855,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/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index b5f7e05..f8ac400 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -839,6 +839,7 @@
if not pro_doc.operations:
pro_doc.set_actual_dates()
+ @frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
@@ -913,6 +914,7 @@
return ret
+ @frappe.whitelist()
def set_items_for_stock_in(self):
self.items = []
@@ -937,6 +939,7 @@
'batch_no': d.batch_no
})
+ @frappe.whitelist()
def get_items(self):
self.set('items', [])
self.validate_work_order()
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/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 7ebd4e6..3296f5b 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -34,7 +34,7 @@
qty=50,
rate=100,
company=company,
- expense_account = "Stock Adjustment - _TC",
+ expense_account = "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
posting_date='2020-04-10',
posting_time='14:00'
)
@@ -46,7 +46,7 @@
qty=10,
rate=200,
company=company,
- expense_account = "Stock Adjustment - _TC",
+ expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
posting_date='2020-04-20',
posting_time='14:00'
)
@@ -58,7 +58,7 @@
target="Finished Goods - _TC",
company=company,
qty=10,
- expense_account="Stock Adjustment - _TC",
+ expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
posting_date='2020-04-30',
posting_time='14:00'
)
@@ -90,7 +90,7 @@
qty=50,
rate=150,
company=company,
- expense_account = "Stock Adjustment - _TC",
+ expense_account ="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC",
posting_date='2020-04-12',
posting_time='14:00'
)
@@ -313,8 +313,8 @@
# Set User with Stock User role but not Stock Manager
try:
- frappe.set_user("test@example.com")
user = frappe.get_doc("User", "test@example.com")
+ frappe.set_user(user.name)
user.add_roles("Stock User")
user.remove_roles("Stock Manager")
@@ -325,7 +325,9 @@
# Block back-dated entry
self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+ frappe.set_user("Administrator")
user.add_roles("Stock Manager")
+ frappe.set_user(user.name)
# Back dated entry allowed to Stock Manager
back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
@@ -337,6 +339,7 @@
finally:
frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
frappe.set_user("Administrator")
+ user.remove_roles("Stock Manager")
def create_repack_entry(**args):
@@ -400,4 +403,4 @@
make_item(d, properties=properties)
- return items
\ No newline at end of file
+ return items
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/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 70e4c2c..e23f7d4 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -110,7 +110,7 @@
get_gross_profit(out)
if args.doctype == 'Material Request':
out.rate = args.rate or out.price_list_rate
- out.amount = flt(args.qty * out.rate)
+ out.amount = flt(args.qty) * flt(out.rate)
return out
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index ff603fc..623dc2f 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -49,7 +49,7 @@
for batch in fifo_queue:
batch_age = date_diff(to_date, batch[1])
- if type(batch[0]) in ['int', 'float']:
+ if isinstance(batch[0], (int, float)):
age_qty += batch_age * batch[0]
total_qty += batch[0]
else:
@@ -302,4 +302,4 @@
fieldname=fieldname,
fieldtype=fieldtype,
width=width
- ))
\ No newline at end of file
+ ))
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 bbbbc4a..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:
@@ -165,6 +165,7 @@
communication.ignore_mandatory = True
communication.save()
+ @frappe.whitelist()
def split_issue(self, subject, communication_id):
# Bug: Pressing enter doesn't send subject
from copy import deepcopy
@@ -259,6 +260,7 @@
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
+ @frappe.whitelist()
def reset_service_level_agreement(self, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
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
+};
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index f5adbf0..167c848 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -11,7 +11,7 @@
<small class="formatted-price">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
</div>
{% else %}
- {{ _("Unit of Measurement") }} : {{ product_info.uom }}
+ {{ _("UOM") }} : {{ product_info.uom }}
{% endif %}
{% if cart_settings.show_stock_availability %}
diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html
index 930d0c2..3834131 100644
--- a/erpnext/templates/includes/transaction_row.html
+++ b/erpnext/templates/includes/transaction_row.html
@@ -14,11 +14,7 @@
</div>
</div>
<div class="col-sm-3 text-right bold">
- {% if doc.doctype == "Quotation" and not doc.docstatus %}
- {{ _("Pending") }}
- {% else %}
- {{ doc.get_formatted("grand_total") }}
- {% endif %}
+ {{ doc.get_formatted("grand_total") }}
</div>
</div>
<a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a>
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 07dd676..28faea8 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -12,21 +12,22 @@
{% endblock %}
{% block header_actions %}
- <div class="dropdown">
- <button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
- <span>{{ _('Actions') }}</span>
- <b class="caret"></b>
- </button>
- <ul class="dropdown-menu dropdown-menu-right" role="menu">
- {% if doc.doctype == 'Purchase Order' %}
- <a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
- {% endif %}
- <a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
- target="_blank" rel="noopener noreferrer">
- {{ _("Print") }}
- </a>
- </ul>
- </div>
+<div class="dropdown">
+ <button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
+ <span>{{ _('Actions') }}</span>
+ <b class="caret"></b>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right" role="menu">
+ {% if doc.doctype == 'Purchase Order' %}
+ <a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
+ {% endif %}
+ <a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
+ target="_blank" rel="noopener noreferrer">
+ {{ _("Print") }}
+ </a>
+ </ul>
+</div>
+
{% endblock %}
{% block page_content %}
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index c8ae733..f99da58 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -7,7 +7,6 @@
from frappe import _
from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_datetime, get_link_to_form, date_diff, nowdate
from erpnext.controllers.status_updater import StatusUpdater
-from erpnext.accounts.utils import get_fiscal_year
from six import string_types
@@ -121,11 +120,11 @@
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
if self.doctype in buying_doctypes:
- to_disable = "Maintain same rate throughout Purchase cycle"
- settings_page = "Buying Settings"
+ action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
+ settings_doc = "Buying Settings"
else:
- to_disable = "Maintain same rate throughout Sales cycle"
- settings_page = "Selling Settings"
+ action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
+ settings_doc = "Selling Settings"
for ref_dt, ref_dn_field, ref_link_field in ref_details:
for d in self.get("items"):
@@ -133,11 +132,16 @@
ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01:
- frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ")
- .format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
- frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.")
- .format(frappe.bold(_(to_disable)),
- get_link_to_form(settings_page, settings_page, frappe.bold(settings_page))))
+ if action == "Stop":
+ role_allowed_to_override = frappe.db.get_single_value(settings_doc, 'role_to_override_stop_action')
+
+ if role_allowed_to_override not in frappe.get_roles():
+ frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
+ d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate))
+ else:
+ frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
+ d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate), title=_("Warning"), indicator="orange")
+
def get_link_filters(self, for_doctype):
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):