diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 3b14e4efa0..ce149f96e6 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 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
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 @@ class TestBankTransaction(unittest.TestCase):
# 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 @@ class TestBankTransaction(unittest.TestCase):
# 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 @@ class TestBankTransaction(unittest.TestCase):
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 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
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 @@ def add_transactions():
}).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 @@ def add_vouchers():
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 @@ def add_vouchers():
})
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/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 03c3eb0ac0..f96f59169e 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 @@ def validate_accounts(file_name):
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 += "
"
+ 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/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index ce76d0a39c..78febf9c2e 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -290,4 +290,8 @@ def rename_temporarily_named_docs(doctype):
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/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index b5f6a401df..c2e804e441 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -637,13 +637,13 @@ frappe.ui.form.on('Payment Entry', {
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 @@ frappe.ui.form.on('Payment Entry', {
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 @@ frappe.ui.form.on('Payment Entry', {
} 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 @@ frappe.ui.form.on('Payment Entry', {
}
$.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 @@ frappe.ui.form.on('Payment Entry', {
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 8acd92cb6b..62ab76c323 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -333,33 +333,50 @@ class PaymentEntry(AccountsController):
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, 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'))
- for key, amount in iteritems(invoice_payment_amount_map):
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 @@ def get_outstanding_reference_documents(args):
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 @@ def get_outstanding_reference_documents(args):
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 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
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 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
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 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
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 772fc1a252..4641d6b5ff 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 @@ class TestPaymentEntry(unittest.TestCase):
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 @@ def create_payment_terms_template():
}]
}).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 8f5e9fbc28..912ad0977a 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_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
index d363cf161b..e362566af0 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 054c2d1191..acd0144c2e 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 e77c244d3d..aec4965d79 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 f5c5bca87a..84c8d09b16 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 2b2b6afe79..80e3348d81 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 @@ from frappe import _
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 @@ class PaymentTermsTemplate(Document):
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 eee3223314..20b3dca6aa 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_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 054afe5bbb..6d388c4aaa 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.purchase_receipt.test_purchase_receipt import make_pu
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/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json
index e7166c5a12..9f9e90d8a7 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/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 720a9175e6..d382386a32 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/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index e00a58f864..3781f8ccc9 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 90e21444f5..f09cc5af96 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2115,6 +2115,7 @@ def create_sales_invoice(**args):
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 632e30db45..ac1ffd9e75 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 @@ test_records = frappe.get_test_records('Tax Rule')
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 51fc7ec49a..444b40ed79 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -364,7 +364,7 @@ class ReceivablePayableReport(object):
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 @@ class ReceivablePayableReport(object):
"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/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 735c31c423..ef9372eeb6 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -253,6 +253,7 @@ class PurchaseOrder(BuyingController):
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()
@@ -367,7 +368,6 @@ def make_purchase_receipt(source_name, target_doc=None):
"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 604c88682f..3c4f908ee4 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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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 @@ class TestPurchaseOrder(unittest.TestCase):
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/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 6e6eaed95d..2528240549 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 @@ import unittest
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 @@ valid_scorecard = [
{
"weight":100.0,
"doctype":"Supplier Scorecard Scoring Criteria",
- "criteria_name":"Delivery"
+ "criteria_name":"Delivery",
+ "formula": "100"
}
],
"supplier":"_Test Supplier",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6cae69676f..36d399cf19 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -923,7 +923,8 @@ class AccountsController(TransactionBase):
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]
@@ -1238,18 +1239,24 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat
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
@@ -1261,6 +1268,16 @@ def get_due_date(term, posting_date=None, bill_date=None):
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):
"""
@@ -1319,25 +1336,63 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
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]
@@ -1346,23 +1401,17 @@ def validate_and_delete_children(parent, data):
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/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index e329b325b3..5f73c55836 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -113,10 +113,10 @@ class calculate_taxes_and_totals(object):
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:
- item.discount_amount = item.rate_with_margin - item.rate
- elif not item.discount_percentage:
+ if item.discount_amount and not item.discount_percentage:
item.rate -= item.discount_amount
+ else:
+ item.discount_amount = item.rate_with_margin - item.rate
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:
@@ -808,4 +808,4 @@ class init_landed_taxes_and_totals(object):
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/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index 3c906374c4..e2243eabde 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 @@ class TestPlaidSettings(unittest.TestCase):
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/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
index a21caca8ff..21776d2380 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 @@ class TestInpatientMedicationOrder(unittest.TestCase):
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/hooks.py b/erpnext/hooks.py
index c2798a36b6..2e26fd2a9b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -260,7 +260,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
- "on_trash": "erpnext.regional.check_deletion_permission"
+ "on_trash": "erpnext.regional.check_deletion_permission",
+ "validate": [
+ "erpnext.regional.india.utils.validate_document_name"
+ ]
},
"Purchase Invoice": {
"validate": [
@@ -282,9 +285,6 @@ doc_events = {
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
- ('Sales Invoice', 'Purchase Invoice'): {
- 'validate': ['erpnext.regional.india.utils.validate_document_name']
- },
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 7a9727f18c..aa5a67f40c 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 @@ class CompensatoryLeaveRequest(Document):
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 @@ class CompensatoryLeaveRequest(Document):
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 629bc57118..ed7d588434 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -80,6 +80,7 @@ class Employee(NestedSet):
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 @@ class Employee(NestedSet):
user.save()
+ def update_approver_role(self):
+ if self.leave_approver:
+ user = frappe.get_doc("User", self.leave_approver)
+ user.flags.ignore_permissions = True
+ user.add_roles("Leave Approver")
+
+ if self.expense_approver:
+ user = frappe.get_doc("User", self.expense_approver)
+ user.flags.ignore_permissions = True
+ user.add_roles("Expense Approver")
+
def validate_date(self):
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
throw(_("Date of Birth cannot be greater than today."))
@@ -503,7 +515,7 @@ def has_user_permission_for_employee(user_name, employee_name):
})
def has_upload_permission(doc, ptype='read', user=None):
- if not user:
+ if not user:
user = frappe.session.user
if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
return True
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json
index cf6b5404ec..04f98d1441 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 14:42:47.321368",
"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 e7bb6dcd48..5010fc3f75 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -6,7 +6,7 @@ import frappe, erpnext
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 @@ class ExpenseClaim(AccountsController):
elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected'
+ def on_update(self):
+ share_doc_with_approver(self, self.expense_approver)
+
def set_payable_account(self):
if not self.payable_account and not self.is_paid:
self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account')
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index f9e3a441bf..3f22ca2141 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 @@ class TestExpenseClaim(unittest.TestCase):
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 @@ class TestExpenseClaim(unittest.TestCase):
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 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco
currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center'])
expense_claim = {
- "doctype": "Expense Claim",
- "employee": employee,
- "payable_account": payable_account,
- "approval_status": "Approved",
- "company": company,
- 'currency': currency,
- "expenses": [{
+ "doctype": "Expense Claim",
+ "employee": employee,
+ "payable_account": payable_account,
+ "approval_status": "Approved",
+ "company": company,
+ "currency": currency,
+ "expenses": [{
"expense_type": "Travel",
"default_account": account,
"currency": currency,
"amount": amount,
"sanctioned_amount": sanctioned_amount,
"cost_center": cost_center
- }]
- }
+ }]
+ }
if taxes:
expense_claim.update(taxes)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 350ceadccd..0bf551e178 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -6,7 +6,7 @@ import frappe
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 @@ class LeaveApplication(Document):
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 @@ class LeaveApplication(Document):
))
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 b335c48594..b54c9712c8 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 frappe.utils import add_days, nowdate, now_datetime, getdate, add_months
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 @@ class TestLeaveApplication(unittest.TestCase):
@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 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
# 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 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertTrue(leave_application.insert())
@@ -393,7 +396,7 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
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 @@ class TestLeaveApplication(unittest.TestCase):
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
leave_application.submit()
@@ -565,6 +568,48 @@ class TestLeaveApplication(unittest.TestCase):
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 83eeae3adb..dcb587407d 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 14:45:27.948207",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",
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 63559c4f5a..cf13036181 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 @@ def validate_leave_allocation_against_leave_application(ledger):
""", (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 @@ def create_leave_ledger_entry(ref_doc, args, submit=True):
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/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index 473193d5ac..177c45edc6 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -7,6 +7,7 @@ import frappe
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 @@ class ShiftRequest(Document):
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 @@ class ShiftRequest(Document):
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 230bb2b0e4..9c0d8e3198 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 @@ from __future__ import unicode_literals
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 @@ class TestShiftRequest(unittest.TestCase):
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 @@ class TestShiftRequest(unittest.TestCase):
shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')})
self.assertEqual(shift_assignment_doc.docstatus, 2)
+ def test_shift_request_approver_perms(self):
+ employee = frappe.get_doc("Employee", "_T-Employee-00001")
+ user = "test_approver_perm_emp@example.com"
+ make_employee(user, "_Test Company")
+
+ # set approver for employee
+ employee.reload()
+ employee.shift_request_approver = user
+ employee.save()
+
+ shift_request = make_shift_request(user, do_not_submit=True)
+ self.assertTrue(shift_request.name in frappe.share.get_shared("Shift Request", user))
+
+ # check shared doc revoked
+ shift_request.reload()
+ department = frappe.get_value("Employee", "_T-Employee-00001", "department")
+ set_shift_approver(department)
+ department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0]
+ shift_request.approver = department_approver
+ shift_request.save()
+ self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user))
+
+ shift_request.reload()
+ shift_request.approver = user
+ shift_request.save()
+
+ frappe.set_user(user)
+ shift_request.reload()
+ shift_request.status = "Approved"
+ shift_request.submit()
+
+ # unset approver
+ frappe.set_user("Administrator")
+ employee.reload()
+ employee.shift_request_approver = ""
+ employee.save()
+
+
def set_shift_approver(department):
department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save()
department_doc.reload()
+
+def make_shift_request(approver, do_not_submit=0):
+ shift_request = frappe.get_doc({
+ "doctype": "Shift Request",
+ "shift_type": "Day Shift",
+ "company": "_Test Company",
+ "employee": "_T-Employee-00001",
+ "employee_name": "_Test Employee",
+ "from_date": nowdate(),
+ "to_date": add_days(nowdate(), 10),
+ "approver": approver,
+ "status": "Approved"
+ }).insert()
+
+ if do_not_submit:
+ return shift_request
+
+ shift_request.submit()
+ return shift_request
\ No newline at end of file
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 0c4c1cafb0..190eb4f10a 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -504,3 +504,25 @@ def grant_leaves_automatically():
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 f650b24d86..f4b56a0e17 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": [
@@ -226,42 +227,12 @@
"onboard": 0,
"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"
},
@@ -383,16 +384,6 @@
"onboard": 0,
"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,
@@ -420,6 +411,15 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "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,
@@ -464,6 +464,15 @@
"onboard": 0,
"type": "Card Break"
},
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Driver",
+ "link_to": "Driver",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
{
"dependencies": "",
"hidden": 0,
@@ -541,6 +550,24 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "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,
@@ -625,33 +652,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "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,
@@ -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_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 2b5df4be24..86ea59dc24 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,25 @@
"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",
+ "label": "Total Penalty Paid",
+ "options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-05 10:06:58.792841",
+ "modified": "2021-04-05 13:45:19.137896",
"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 bac06c4e9e..5d57cedb41 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 @@ class LoanRepayment(AccountsController):
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,16 +61,29 @@ class LoanRepayment(AccountsController):
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(getdate(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:
+ if not self.shortfall_amount and self.amount_paid < self.penalty_amount:
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg)
@@ -148,11 +162,28 @@ class LoanRepayment(AccountsController):
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 +208,7 @@ class LoanRepayment(AccountsController):
'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 +224,20 @@ class LoanRepayment(AccountsController):
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.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 +252,8 @@ class LoanRepayment(AccountsController):
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,
@@ -284,7 +315,9 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
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,12 +328,13 @@ def get_accrued_interest_entries(against_loan):
`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
@@ -312,7 +346,7 @@ def get_amounts(amounts, against_loan, posting_date):
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)
pending_accrual_entries = {}
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 102bc0d71d..99b5c72b2d 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 b5e78981d0..8233b7b297 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 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled
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 @@ def update_shortfall_status(loan, security_value):
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 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
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 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
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 @@ def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_
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 @@ def update_pending_shortfall(shortfall):
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/salary_slip_loan/salary_slip_loan.json b/erpnext/loan_management/doctype/salary_slip_loan/salary_slip_loan.json
index 2f4fe24945..3d07081215 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 0f72c3cce7..2a74a1eb85 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 @@ def get_active_loan_details(filters):
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/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3239478872..cd61d2a31d 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 @@ class TestBOM(unittest.TestCase):
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
- 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.assertEqual(bom.operating_cost, op_cost)
+ self.assertEqual(bom.raw_material_cost, raw_material_cost)
+ self.assertEqual(bom.total_cost, raw_material_cost + op_cost)
+
+ # test amounts in selected currency
+ self.assertEqual(bom.base_operating_cost, base_op_cost)
+ self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost)
+ self.assertEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
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 @@ class TestBOM(unittest.TestCase):
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 8aa0ffd774..92074c6288 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -47,6 +47,8 @@ class JobCard(Document):
if d.completed_qty:
self.total_completed_qty += d.completed_qty
+ self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
+
def get_overlap_for(self, args, check_next_available_slot=False):
production_capacity = 1
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 15ec6209c1..288c1d0cd6 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -25,6 +25,16 @@ frappe.ui.form.on('Production Plan', {
}
});
+ 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 @@ cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = fu
['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 05b328c9e8..cef2d8be7a 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -70,7 +70,7 @@ class ProductionPlan(Document):
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))
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 7071bc1ab0..6a38dcfa03 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.workstation.test_workstation import make_work
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 @@ class TestRouting(unittest.TestCase):
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)
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 08291d1eae..6b1fafe5f4 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 @@ class TestWorkOrder(unittest.TestCase):
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 @@ class TestWorkOrder(unittest.TestCase):
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/patches.txt b/erpnext/patches.txt
index 46f0d4ae79..16863142bc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -99,7 +99,7 @@ execute:frappe.delete_doc("DocType", "Purchase Request")
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.update_item_description_based_on_item_master
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,6 +752,7 @@ erpnext.patches.v13_0.set_company_in_leave_ledger_entry
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
@@ -762,3 +763,4 @@ 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.v13_0.rename_discharge_date_in_ip_record
+erpnext.patches.v12_0.purchase_receipt_status
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 0000000000..1a99b3163b
--- /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/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
index de08aa26b3..2d3b096915 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,8 @@ def execute():
if "Healthcare" not in frappe.get_active_domains():
return
+ frappe.reload_doc("healthcare", "doctype", "Therapy Session")
+ frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
frappe.reload_doc("healthcare", "doctype", "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/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py
new file mode 100644
index 0000000000..4816b40250
--- /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/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json
index 2b29f667fb..61ae7e4c2f 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 14:45:48.566756",
"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 4c45580bf0..c6f764ccdb 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 14:46:22.465521",
"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 ea9ccd5205..e1f8431ec5 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 @@ frappe.ui.form.on('Employee Benefit Claim', {
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 da24aacda1..e331b7af93 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 15:51:51.489269",
"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 e5b1052b3a..51346c6c7d 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 14:48:00.919839",
"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 0e0c9b5a1a..fb11875e96 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 @@ frappe.ui.form.on('Employee Tax Exemption Declaration', {
});
}).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 83d4ae53df..873bf887bf 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 20:41:57.387749",
"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 497f35c41e..4fb0a3771e 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 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', {
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 53f18cb1fe..f32202a3bd 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 20:48:32.639885",
"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 9fa261dea2..c343a44326 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 20:53:33.323712",
"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 395e56fa92..85bb651af7 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -133,45 +133,59 @@ frappe.ui.form.on('Payroll Entry', {
}
};
});
+
+ 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 @@ let render_employee_attendance = function (frm, data) {
})
);
};
-
-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 78904710a8..fde2e0776e 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.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT,
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 @@ class PayrollEntry(Document):
condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency}
sal_struct = frappe.db.sql_list("""
- select
- name from `tabSalary Structure`
- where
- docstatus = 1 and
- is_active = 'Yes'
- and company = %(company)s
- and currency = %(currency)s and
- ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
- {condition}""".format(condition=condition),
- {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
+ select
+ name from `tabSalary Structure`
+ where
+ docstatus = 1 and
+ is_active = 'Yes'
+ and company = %(company)s
+ and currency = %(currency)s and
+ ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
+ {condition}""".format(condition=condition),
+ {"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
if sal_struct:
cond += "and t2.salary_structure IN %(sal_struct)s "
@@ -176,15 +177,15 @@ class PayrollEntry(Document):
"""
Returns list of salary slips based on selected criteria
"""
- cond = self.get_filter_condition()
ss_list = frappe.db.sql("""
select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1
- where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s
- and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s %s
- """ % ('%s', '%s', '%s','%s', cond), (ss_status, self.start_date, self.end_date, self.salary_slip_based_on_timesheet), as_dict=as_dict)
+ where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s
+ and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s
+ """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict)
return ss_list
+ @frappe.whitelist()
def submit_salary_slips(self):
self.check_permission('write')
ss_list = self.get_sal_slip_list(ss_status=0)
@@ -270,26 +271,26 @@ class PayrollEntry(Document):
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount += flt(amount, precision)
accounts.append({
- "account": acc_cc[0],
- "debit_in_account_currency": flt(amt, precision),
- "exchange_rate": flt(exchange_rate),
- "party_type": '',
- "cost_center": acc_cc[1] or self.cost_center,
- "project": self.project
- })
+ "account": acc_cc[0],
+ "debit_in_account_currency": flt(amt, precision),
+ "exchange_rate": flt(exchange_rate),
+ "party_type": '',
+ "cost_center": acc_cc[1] or self.cost_center,
+ "project": self.project
+ })
# Deductions
for acc_cc, amount in deductions.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount -= flt(amount, precision)
accounts.append({
- "account": acc_cc[0],
- "credit_in_account_currency": flt(amt, precision),
- "exchange_rate": flt(exchange_rate),
- "cost_center": acc_cc[1] or self.cost_center,
- "party_type": '',
- "project": self.project
- })
+ "account": acc_cc[0],
+ "credit_in_account_currency": flt(amt, precision),
+ "exchange_rate": flt(exchange_rate),
+ "cost_center": acc_cc[1] or self.cost_center,
+ "party_type": '',
+ "project": self.project
+ })
# Payable amount
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
@@ -335,10 +336,9 @@ class PayrollEntry(Document):
def make_payment_entry(self):
self.check_permission('write')
- cond = self.get_filter_condition()
salary_slip_name_list = frappe.db.sql(""" select t1.name from `tabSalary Slip` t1
- where t1.docstatus = 1 and start_date >= %s and end_date <= %s %s
- """ % ('%s', '%s', cond), (self.start_date, self.end_date), as_list = True)
+ where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s
+ """, (self.start_date, self.end_date, self.name), as_list = True)
if salary_slip_name_list and len(salary_slip_name_list) > 0:
salary_slip_total = 0
@@ -370,20 +370,20 @@ class PayrollEntry(Document):
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
accounts.append({
- "account": self.payment_account,
- "bank_account": self.bank_account,
- "credit_in_account_currency": flt(amount, precision),
- "exchange_rate": flt(exchange_rate),
- })
+ "account": self.payment_account,
+ "bank_account": self.bank_account,
+ "credit_in_account_currency": flt(amount, precision),
+ "exchange_rate": flt(exchange_rate),
+ })
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
accounts.append({
- "account": payroll_payable_account,
- "debit_in_account_currency": flt(amount, precision),
- "exchange_rate": flt(exchange_rate),
- "reference_type": self.doctype,
- "reference_name": self.name
- })
+ "account": payroll_payable_account,
+ "debit_in_account_currency": flt(amount, precision),
+ "exchange_rate": flt(exchange_rate),
+ "reference_type": self.doctype,
+ "reference_name": self.name
+ })
if len(currencies) > 1:
multi_currency = 1
@@ -409,6 +409,7 @@ class PayrollEntry(Document):
self.update(get_start_end_dates(self.payroll_frequency,
self.start_date or self.posting_date, self.company))
+ @frappe.whitelist()
def validate_employee_attendance(self):
employees_to_mark_attendance = []
days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0
@@ -424,7 +425,7 @@ class PayrollEntry(Document):
employees_to_mark_attendance.append({
"employee": employee_detail.employee,
"employee_name": employee_detail.employee_name
- })
+ })
return employees_to_mark_attendance
def get_count_holidays_of_employee(self, employee, start_date):
@@ -441,11 +442,11 @@ class PayrollEntry(Document):
def get_count_employee_attendance(self, employee, start_date):
marked_days = 0
attendances = frappe.get_all("Attendance",
- fields = ["count(*)"],
- filters = {
- "employee": employee,
- "attendance_date": ('between', [start_date, self.end_date])
- }, as_list=1)
+ fields = ["count(*)"],
+ filters = {
+ "employee": employee,
+ "attendance_date": ('between', [start_date, self.end_date])
+ }, as_list=1)
if attendances and attendances[0][0]:
marked_days = attendances[0][0]
return marked_days
@@ -553,6 +554,7 @@ def payroll_entry_has_bank_entries(name):
def create_salary_slips_for_employees(employees, args, publish_progress=True):
salary_slips_exists_for = get_existing_salary_slips(employees, args)
count=0
+ salary_slips_not_created = []
for emp in employees:
if emp not in salary_slips_exists_for:
args.update({
@@ -566,33 +568,24 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True):
frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)),
title = _("Creating Salary Slips..."))
else:
- salary_slip_name = frappe.db.sql(
- '''SELECT
- name
- FROM `tabSalary Slip`
- WHERE company=%s
- AND start_date >= %s
- AND end_date <= %s
- AND employee = %s
- ''', (args.company, args.start_date, args.end_date, emp), as_dict=True)
-
- salary_slip_doc = frappe.get_doc('Salary Slip', salary_slip_name[0].name)
- salary_slip_doc.exchange_rate = args.exchange_rate
- salary_slip_doc.set_totals()
- salary_slip_doc.db_update()
+ salary_slips_not_created.append(emp)
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
payroll_entry.db_set("salary_slips_created", 1)
payroll_entry.notify_update()
+ if salary_slips_not_created:
+ frappe.msgprint(_("Salary Slips already exists for employees {}, and will not be processed by this payroll.")
+ .format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))) , title=_("Message"), indicator="orange")
+
def get_existing_salary_slips(employees, args):
return frappe.db.sql_list("""
select distinct employee from `tabSalary Slip`
- where docstatus!= 2 and company = %s
+ where docstatus!= 2 and company = %s and payroll_entry = %s
and start_date >= %s and end_date <= %s
and employee in (%s)
- """ % ('%s', '%s', '%s', ', '.join(['%s']*len(employees))),
- [args.company, args.start_date, args.end_date] + employees)
+ """ % ('%s', '%s', '%s', '%s', ', '.join(['%s']*len(employees))),
+ [args.company, args.payroll_entry, args.start_date, args.end_date] + employees)
def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
submitted_ss = []
@@ -644,3 +637,61 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
'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 84c381489c..7528bf7a7f 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 @@ class TestPayrollEntry(unittest.TestCase):
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 6647230078..cd563bc404 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 14:50:29.401020",
"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 d5278393a1..e3993fae3a 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -39,7 +39,8 @@ frappe.ui.form.on("Salary Slip", {
frm.set_query("employee", function() {
return {
- query: "erpnext.controllers.queries.employee_query"
+ query: "erpnext.controllers.queries.employee_query",
+ filters: frm.doc.company
};
});
},
@@ -93,28 +94,31 @@ frappe.ui.form.on("Salary Slip", {
},
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 +220,7 @@ frappe.ui.form.on('Salary Slip Timesheet', {
});
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 6688368262..ec5607602d 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 15:39:28.817166",
"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 9abe57cd65..f6d4c7b855 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -124,9 +124,12 @@ class SalarySlip(TransactionBase):
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 = ''
@@ -618,13 +621,16 @@ class SalarySlip(TransactionBase):
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
@@ -1050,7 +1056,7 @@ class SalarySlip(TransactionBase):
repayment_entry.save()
repayment_entry.submit()
- loan.loan_repayment_entry = repayment_entry.name
+ frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
def cancel_loan_repayment_entry(self):
for loan in self.loans:
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json
index de56fc8457..5dd1d701f0 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 1712081550..352c1804f0 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -100,7 +100,7 @@ class SalaryStructure(Document):
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_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index 92bb347661..50fabedb42 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 15:49:36.361253",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 077011ace0..c5265e23c0 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -18,8 +18,8 @@ frappe.ui.form.on("Project", {
};
},
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/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 32d371d682..6c2144d6cb 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -737,28 +737,34 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
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 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
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/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
index 888b2da48e..369a4001ef 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 @@