diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..399b176e1d --- /dev/null +++ b/.flake8 @@ -0,0 +1,32 @@ +[flake8] +ignore = + E121, + E126, + E127, + E128, + E203, + E225, + E226, + E231, + E241, + E251, + E261, + E265, + E302, + E303, + E305, + E402, + E501, + E741, + W291, + W292, + W293, + W391, + W503, + W504, + F403, + B007, + B950, + W191, + +max-line-length = 200 \ No newline at end of file diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 253ad70198..7b0f944c66 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -12,7 +12,7 @@ sudo apt install npm pip install frappe-bench -git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF}" --depth 1 +git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1 bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench mkdir ~/frappe-bench/sites/test_site @@ -43,4 +43,4 @@ sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app erpnext "${GITHUB_WORKSPACE}" bench start & -bench --site test_site reinstall --yes \ No newline at end of file +bench --site test_site reinstall --yes diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 2a1db14d95..78c2f5a187 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -1,12 +1,10 @@ name: CI -on: - pull_request: - workflow_dispatch: +on: [pull_request, workflow_dispatch, push] jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 strategy: fail-fast: false diff --git a/README.md b/README.md index 15782a2e0c..bb592ae75c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@
ERP made simple
-[![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext) +[![CI](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop) diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 022d7a7e80..10cd939894 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -11,36 +11,36 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice class TestAccountingPeriod(unittest.TestCase): - def test_overlap(self): - ap1 = create_accounting_period(start_date = "2018-04-01", - end_date = "2018-06-30", company = "Wind Power LLC") - ap1.save() + def test_overlap(self): + ap1 = create_accounting_period(start_date = "2018-04-01", + end_date = "2018-06-30", company = "Wind Power LLC") + ap1.save() - ap2 = create_accounting_period(start_date = "2018-06-30", - end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") - self.assertRaises(OverlapError, ap2.save) + ap2 = create_accounting_period(start_date = "2018-06-30", + end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") + self.assertRaises(OverlapError, ap2.save) - def test_accounting_period(self): - ap1 = create_accounting_period(period_name = "Test Accounting Period 2") - ap1.save() + def test_accounting_period(self): + ap1 = create_accounting_period(period_name = "Test Accounting Period 2") + ap1.save() - doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC") - self.assertRaises(ClosedAccountingPeriod, doc.submit) + doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") + self.assertRaises(ClosedAccountingPeriod, doc.submit) - def tearDown(self): - for d in frappe.get_all("Accounting Period"): - frappe.delete_doc("Accounting Period", d.name) + def tearDown(self): + for d in frappe.get_all("Accounting Period"): + frappe.delete_doc("Accounting Period", d.name) def create_accounting_period(**args): - args = frappe._dict(args) + args = frappe._dict(args) - accounting_period = frappe.new_doc("Accounting Period") - accounting_period.start_date = args.start_date or nowdate() - accounting_period.end_date = args.end_date or add_months(nowdate(), 1) - accounting_period.company = args.company or "_Test Company" - accounting_period.period_name =args.period_name or "_Test_Period_Name_1" - accounting_period.append("closed_documents", { - "document_type": 'Sales Invoice', "closed": 1 - }) + accounting_period = frappe.new_doc("Accounting Period") + accounting_period.start_date = args.start_date or nowdate() + accounting_period.end_date = args.end_date or add_months(nowdate(), 1) + accounting_period.company = args.company or "_Test Company" + accounting_period.period_name =args.period_name or "_Test_Period_Name_1" + accounting_period.append("closed_documents", { + "document_type": 'Sales Invoice', "closed": 1 + }) - return accounting_period \ No newline at end of file + return accounting_period diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index bdfe532b9f..8d6de2d562 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -6,10 +6,12 @@ from __future__ import unicode_literals import frappe import unittest -test_dependencies = ["Customer", "Supplier"] +from frappe.cache_manager import clear_doctype_cache from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account +test_dependencies = ["Customer", "Supplier"] + class TestOpeningInvoiceCreationTool(unittest.TestCase): def setUp(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): @@ -24,22 +26,25 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): def test_opening_sales_invoice_creation(self): property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - invoices = self.make_invoices(company="_Test Opening Invoice Company") + try: + invoices = self.make_invoices(company="_Test Opening Invoice Company") - self.assertEqual(len(invoices), 2) - expected_value = { - "keys": ["customer", "outstanding_amount", "status"], - 0: ["_Test Customer", 300, "Overdue"], - 1: ["_Test Customer 1", 250, "Overdue"], - } - self.check_expected_values(invoices, expected_value) + self.assertEqual(len(invoices), 2) + expected_value = { + "keys": ["customer", "outstanding_amount", "status"], + 0: ["_Test Customer", 300, "Overdue"], + 1: ["_Test Customer 1", 250, "Overdue"], + } + self.check_expected_values(invoices, expected_value) - si = frappe.get_doc("Sales Invoice", invoices[0]) + si = frappe.get_doc("Sales Invoice", invoices[0]) - # Check if update stock is not enabled - self.assertEqual(si.update_stock, 0) + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) - property_setter.delete() + finally: + property_setter.delete() + clear_doctype_cache("Sales Invoice") def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" @@ -143,4 +148,4 @@ def make_customer(customer=None): customer.insert(ignore_permissions=True) return customer.name else: - return frappe.db.exists("Customer", customer_name) \ No newline at end of file + return frappe.db.exists("Customer", customer_name) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6412772073..b5f6a401df 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -605,12 +605,22 @@ frappe.ui.form.on('Payment Entry', { {fieldtype:"Column Break"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, {fieldtype:"Section Break"}, + {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center", + "get_query": function() { + return { + "filters": {"company": frm.doc.company} + } + } + }, + {fieldtype:"Column Break"}, + {fieldtype:"Section Break"}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, ]; frappe.prompt(fields, function(filters){ frappe.flags.allocate_payment_amount = true; frm.events.validate_filters_data(frm, filters); + frm.doc.cost_center = filters.cost_center; frm.events.get_outstanding_documents(frm, filters); }, __("Filters"), __("Get Outstanding Documents")); }, @@ -1066,11 +1076,6 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance); frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); frm.set_value("party_balance", r.message.party_balance); - }, - () => { - if(frm.doc.payment_type != "Internal") { - frm.clear_table("references"); - } } ]); diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 40db09ec3b..b596c0cf25 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -5,12 +5,21 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import nowdate +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPOSClosingEntry(unittest.TestCase): + def setUp(self): + # Make stock available for POS Sales + make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + def test_pos_closing_entry(self): test_user, pos_profile = init_user_and_profile() opening_entry = create_opening_entry(pos_profile, test_user.name) @@ -41,9 +50,6 @@ class TestPOSClosingEntry(unittest.TestCase): self.assertEqual(pcv_doc.total_quantity, 2) self.assertEqual(pcv_doc.net_total, 6700) - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - def test_cancelling_of_pos_closing_entry(self): test_user, pos_profile = init_user_and_profile() opening_entry = create_opening_entry(pos_profile, test_user.name) @@ -84,8 +90,6 @@ class TestPOSClosingEntry(unittest.TestCase): self.assertEqual(si_doc.docstatus, 2) self.assertEqual(pos_inv1.status, 'Paid') - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") def init_user_and_profile(**args): user = 'test@example.com' @@ -103,4 +107,4 @@ def init_user_and_profile(**args): pos_profile.save() - return test_user, pos_profile \ No newline at end of file + return test_user, pos_profile diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 76e00923c4..402d157009 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -57,7 +57,7 @@ class POSInvoice(SalesInvoice): self.apply_loyalty_points() self.check_phone_payments() self.set_status(update=True) - + def before_cancel(self): if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: pos_closing_entry = frappe.get_all( @@ -221,7 +221,7 @@ class POSInvoice(SalesInvoice): base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) if not flt(self.change_amount) and grand_total < flt(self.paid_amount): self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) - self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount)) + self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) if flt(self.change_amount) and not self.account_for_change_amount: frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) @@ -377,6 +377,12 @@ class POSInvoice(SalesInvoice): "allow_print_before_pay": profile.get("allow_print_before_pay") } + def reset_mode_of_payments(self): + if self.pos_profile: + pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile) + update_multi_mode_option(self, pos_profile) + self.paid_amount = 0 + def set_account_for_mode_of_payment(self): self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] for pay in self.payments: @@ -400,7 +406,7 @@ class POSInvoice(SalesInvoice): pay_req.request_phone_payment() return pay_req - + def get_new_payment_request(self, mop): payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { "payment_account": mop.account, diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index eb52fd6275..054afe5bbb 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -9,8 +9,16 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.item.test_item import make_item class TestPOSInvoice(unittest.TestCase): + def tearDown(self): + if frappe.session.user != "Administrator": + frappe.set_user("Administrator") + + if frappe.db.get_single_value("Selling Settings", "validate_selling_price"): + frappe.db.set_value("Selling Settings", None, "validate_selling_price", 0) + def test_timestamp_change(self): w = create_pos_invoice(do_not_save=1) w.docstatus = 0 @@ -370,7 +378,6 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.load_from_db() rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 3470) - frappe.set_user("Administrator") def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile @@ -412,7 +419,6 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.load_from_db() rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 840) - frappe.set_user("Administrator") def test_merging_with_validate_selling_price(self): from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile @@ -421,10 +427,12 @@ class TestPOSInvoice(unittest.TestCase): if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) - make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300) + item = "Test Selling Price Validation" + make_item(item, {"is_stock_item": 1}) + make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300) frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() - pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1) pos_inv.append('payments', { 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 }) @@ -438,7 +446,7 @@ class TestPOSInvoice(unittest.TestCase): }) self.assertRaises(frappe.ValidationError, pos_inv.submit) - pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1) + pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1) pos_inv2.append('payments', { 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 }) @@ -457,8 +465,6 @@ class TestPOSInvoice(unittest.TestCase): pos_inv2.load_from_db() rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) - frappe.set_user("Administrator") - frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0) def create_pos_invoice(**args): args = frappe._dict(args) @@ -508,4 +514,4 @@ def create_pos_invoice(**args): else: pos_inv.payment_schedule = [] - return pos_inv \ No newline at end of file + return pos_inv diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index db046c9800..d880caa3c7 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -14,85 +14,89 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): def test_consolidated_invoice_creation(self): frappe.db.sql("delete from `tabPOS Invoice`") - test_user, pos_profile = init_user_and_profile() + try: + test_user, pos_profile = init_user_and_profile() - pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.submit() + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 + }) + pos_inv.submit() - pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) - pos_inv2.submit() + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 + }) + pos_inv2.submit() - pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) - pos_inv3.submit() + pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) + pos_inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 + }) + pos_inv3.submit() - consolidate_pos_invoices() + consolidate_pos_invoices() - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") - def test_consolidated_credit_note_creation(self): frappe.db.sql("delete from `tabPOS Invoice`") - test_user, pos_profile = init_user_and_profile() + try: + test_user, pos_profile = init_user_and_profile() - pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.submit() + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 + }) + pos_inv.submit() - pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) - pos_inv2.submit() + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 + }) + pos_inv2.submit() - pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) - pos_inv3.submit() + pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) + pos_inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 + }) + pos_inv3.submit() - pos_inv_cn = make_sales_return(pos_inv.name) - pos_inv_cn.set("payments", []) - pos_inv_cn.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 - }) - pos_inv_cn.paid_amount = -300 - pos_inv_cn.submit() + pos_inv_cn = make_sales_return(pos_inv.name) + pos_inv_cn.set("payments", []) + pos_inv_cn.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 + }) + pos_inv_cn.paid_amount = -300 + pos_inv_cn.submit() - consolidate_pos_invoices() + consolidate_pos_invoices() - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - pos_inv_cn.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) - self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) + pos_inv_cn.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) + self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 33771645fe..428989aa96 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -44,6 +44,14 @@ "column_break_21", "min_amt", "max_amt", + "product_discount_scheme_section", + "same_item", + "free_item", + "free_qty", + "free_item_rate", + "column_break_42", + "free_item_uom", + "is_recursive", "section_break_23", "valid_from", "valid_upto", @@ -62,13 +70,6 @@ "discount_amount", "discount_percentage", "for_price_list", - "product_discount_scheme_section", - "same_item", - "free_item", - "free_qty", - "column_break_51", - "free_item_uom", - "free_item_rate", "section_break_13", "threshold_percentage", "priority", @@ -458,10 +459,6 @@ "fieldtype": "Float", "label": "Qty" }, - { - "fieldname": "column_break_51", - "fieldtype": "Column Break" - }, { "fieldname": "free_item_uom", "fieldtype": "Link", @@ -552,19 +549,33 @@ "fieldname": "promotional_scheme", "fieldtype": "Link", "label": "Promotional Scheme", - "options": "Promotional Scheme" + "no_copy": 1, + "options": "Promotional Scheme", + "print_hide": 1, + "read_only": 1 }, { "description": "Simple Python Expression, Example: territory != 'All Territories'", "fieldname": "condition", "fieldtype": "Code", "label": "Condition" + }, + { + "fieldname": "column_break_42", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on", + "fieldname": "is_recursive", + "fieldtype": "Check", + "label": "Is Recursive" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-03-01 23:18:38.717613", + "modified": "2021-03-06 22:01:24.840422", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index f0b4e2976d..aedf1c6f1a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -237,6 +237,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "doctype": args.doctype, "has_margin": False, "name": args.name, + "free_item_data": [], "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get('child_docname') diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index d163335996..c676abd4c6 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -367,7 +367,7 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): if items and doc.get("items"): for row in doc.get('items'): - if row.get(apply_on) not in items: continue + if (row.get(apply_on) or args.get(apply_on)) not in items: continue if pr_doc.mixed_conditions: amt = args.get('qty') * args.get("price_list_rate") @@ -479,7 +479,7 @@ def apply_pricing_rule_on_transaction(doc): doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': - item_details = frappe._dict({'parenttype': doc.doctype}) + item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []}) get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() @@ -508,9 +508,16 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): frappe.throw(_("Free item not set in the pricing rule {0}") .format(get_link_to_form("Pricing Rule", pricing_rule.name))) - item_details.free_item_data = { + qty = pricing_rule.free_qty or 1 + if pricing_rule.is_recursive: + transaction_qty = args.get('qty') if args else doc.total_qty + if transaction_qty: + qty = flt(transaction_qty) * qty + + free_item_data_args = { 'item_code': free_item, - 'qty': pricing_rule.free_qty or 1, + 'qty': qty, + 'pricing_rules': pricing_rule.name, 'rate': pricing_rule.free_item_rate or 0, 'price_list_rate': pricing_rule.free_item_rate or 0, 'is_free_item': 1 @@ -519,24 +526,26 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): item_data = frappe.get_cached_value('Item', free_item, ['item_name', 'description', 'stock_uom'], as_dict=1) - item_details.free_item_data.update(item_data) - item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom - item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, - item_details.free_item_data['uom']).get("conversion_factor", 1) + free_item_data_args.update(item_data) + free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom + free_item_data_args['conversion_factor'] = get_conversion_factor(free_item, + free_item_data_args['uom']).get("conversion_factor", 1) if item_details.get("parenttype") == 'Purchase Order': - item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today() + free_item_data_args['schedule_date'] = doc.schedule_date if doc else today() if item_details.get("parenttype") == 'Sales Order': - item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() + free_item_data_args['delivery_date'] = doc.delivery_date if doc else today() + + item_details.free_item_data.append(free_item_data_args) def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): - if pricing_rule_args.get('item_code'): - items = [d.item_code for d in doc.items - if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item] + if pricing_rule_args: + items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item]) - if not items: - doc.append('items', pricing_rule_args) + for args in pricing_rule_args: + if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: + doc.append('items', args) def get_pricing_rule_items(pr_doc): apply_on_data = [] diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 89f7238a06..523e9ee08a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -12,16 +12,16 @@ from frappe.model.document import Document pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group' 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', - 'supplier_group', 'company', 'currency'] + 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] other_fields = ['min_qty', 'max_qty', 'min_amt', 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description'] price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate', - 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule'] + 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules'] product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', - 'free_item_rate', 'same_item'] + 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] class PromotionalScheme(Document): def validate(self): diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json index 224b8de779..795fb1c6f4 100644 --- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json @@ -1,792 +1,181 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-03-24 14:48:59.649168", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "disable", + "apply_multiple_pricing_rules", + "column_break_2", + "rule_description", + "section_break_2", + "min_qty", + "max_qty", + "column_break_3", + "min_amount", + "max_amount", + "section_break_6", + "rate_or_discount", + "column_break_10", + "rate", + "discount_amount", + "discount_percentage", + "section_break_11", + "warehouse", + "threshold_percentage", + "validate_applied_rule", + "column_break_14", + "priority", + "apply_discount_on_rate" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "disable", "fieldtype": "Check", - "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": "Disable", - "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 + "label": "Disable" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rule_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": "Rule Description", - "length": 0, "no_copy": 1, - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_2", - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "default": "0", "fieldname": "min_qty", "fieldtype": "Float", - "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": "Min Qty", - "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 + "label": "Min Qty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "default": "0", "fieldname": "max_qty", "fieldtype": "Float", - "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": "Max Qty", - "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 + "label": "Max Qty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "min_amount", "fieldtype": "Currency", - "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": "Min Amount", - "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 + "label": "Min Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", - "depends_on": "", "fieldname": "max_amount", "fieldtype": "Currency", - "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": "Max Amount", - "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 + "label": "Max Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "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, - "label": "", - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Discount Percentage", - "depends_on": "", "fieldname": "rate_or_discount", "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": "Discount Type", - "length": 0, - "no_copy": 0, - "options": "\nRate\nDiscount Percentage\nDiscount Amount", - "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 + "options": "\nRate\nDiscount Percentage\nDiscount Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "column_break_10", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "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": "Rate", - "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 + "label": "Rate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.rate_or_discount==\"Discount Amount\"", "fieldname": "discount_amount", "fieldtype": "Currency", - "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": "Discount Amount", - "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 + "label": "Discount Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"", "fieldname": "discount_percentage", "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": "Discount Percentage", - "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 + "label": "Discount Percentage" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_11", - "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 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "warehouse", "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": "Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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 + "options": "Warehouse" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "threshold_percentage", "fieldtype": "Percent", - "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": "Threshold for Suggestion", - "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 + "label": "Threshold for Suggestion" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "validate_applied_rule", "fieldtype": "Check", - "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": "Validate Applied Rule", - "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 + "label": "Validate Applied Rule" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_14", - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "priority", "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": "Priority", - "length": 0, - "no_copy": 0, - "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20", - "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 + "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "priority", "fieldname": "apply_multiple_pricing_rules", "fieldtype": "Check", - "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": "Apply Multiple Pricing Rules", - "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 + "label": "Apply Multiple Pricing Rules" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules", "fieldname": "apply_discount_on_rate", "fieldtype": "Check", - "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": "Apply Discount on Rate", - "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 + "label": "Apply Discount on Rate" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, + "index_web_pages_for_search": 1, "istable": 1, - "max_attachments": 0, - "modified": "2019-03-24 14:48:59.649168", + "links": [], + "modified": "2021-03-07 11:56:23.424137", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Price Discount", - "name_case": "", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json index 72d53bfa01..3eab51510d 100644 --- a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json @@ -1,10 +1,12 @@ { + "actions": [], "creation": "2019-03-24 14:48:59.649168", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "disable", + "apply_multiple_pricing_rules", "column_break_2", "rule_description", "section_break_1", @@ -25,7 +27,7 @@ "threshold_percentage", "column_break_15", "priority", - "apply_multiple_pricing_rules" + "is_recursive" ], "fields": [ { @@ -152,10 +154,19 @@ "fieldname": "apply_multiple_pricing_rules", "fieldtype": "Check", "label": "Apply Multiple Pricing Rules" + }, + { + "default": "0", + "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on", + "fieldname": "is_recursive", + "fieldtype": "Check", + "label": "Is Recursive" } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-07-21 00:00:56.674284", + "links": [], + "modified": "2021-03-06 21:58:18.162346", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Product Discount", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 06aa20bfc5..66a8e206a8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -524,7 +524,7 @@ frappe.ui.form.on("Purchase Invoice", { }, onload: function(frm) { - if(frm.doc.__onload) { + if(frm.doc.__onload && frm.is_new()) { if(frm.doc.supplier) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ded293b88d..50492f50b5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -898,7 +898,7 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entries = 1 acc_settings.save() - item = create_item("_Test Item for Deferred Accounting") + item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True) item.enable_deferred_expense = 1 item.deferred_expense_account = deferred_account item.save() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4076be724b..a1bf66b03e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -76,7 +76,7 @@ class SalesInvoice(SellingController): if not self.is_pos: self.so_dn_required() - + self.set_tax_withholding() self.validate_proj_cust() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1b9557839f..90e21444f5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1800,6 +1800,15 @@ class TestSalesInvoice(unittest.TestCase): si.selling_price_list = "_Test Price List Rest of the World" si.update_stock = 1 si.items[0].target_warehouse = 'Work In Progress - TCP1' + + # Add stock to stores for succesful stock transfer + make_stock_entry( + target="Stores - TCP1", + company = "_Test Company with perpetual inventory", + qty=1, + basic_rate=100 + ) + add_taxes(si) si.save() @@ -2269,4 +2278,4 @@ def add_taxes(doc): "cost_center": "Main - TCP1", "description": "Excise Duty", "rate": 12 - }) \ No newline at end of file + }) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 429a9f3591..52d19d54a8 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -46,5 +46,5 @@ def validate_disabled(doc): frappe.throw(_("Disabled template must not be default template")) def validate_for_tax_category(doc): - if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 9ce8e3fe83..dd3b49aa04 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -177,7 +177,7 @@ def cancel_invoices(): for d in purchase_invoices: frappe.get_doc('Purchase Invoice', d).cancel() - + for d in sales_invoices: frappe.get_doc('Sales Invoice', d).cancel() @@ -229,7 +229,8 @@ def create_sales_invoice(**args): 'qty': args.qty or 1, 'rate': args.rate or 10000, 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC' + 'expense_account': 'Cost of Goods Sold - _TC', + 'warehouse': args.warehouse or '_Test Warehouse - _TC' }] }) @@ -353,4 +354,4 @@ def create_tax_with_holding_category(): 'company': '_Test Company', 'account': 'TDS - _TC' }] - }).insert() \ No newline at end of file + }).insert() diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7dfce85629..14efa1f8fc 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -51,7 +51,11 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ "from_date": start_date }) - to_date = add_months(start_date, months_to_add) + if i==0 and filter_based_on == 'Date Range': + to_date = add_months(get_first_day(start_date), months_to_add) + else: + to_date = add_months(start_date, months_to_add) + start_date = to_date # Subtract one day from to_date, as it may be first day in next fiscal year or month diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 02d4865320..604c88682f 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -723,7 +723,7 @@ class TestPurchaseOrder(unittest.TestCase): is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") make_stock_entry(target="_Test Warehouse - _TC", - item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100) + item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code = "Test Extra Item 1", qty=100, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 12a81c7887..73276f3646 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -26,7 +26,8 @@ from erpnext.controllers.print_settings import set_print_templates_for_item_tabl class AccountMissingError(frappe.ValidationError): pass -force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") +force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", + "pricing_rules", "weight_per_unit", "weight_uom", "total_weight") class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): @@ -1394,7 +1395,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) def get_new_child_item(item_row): - child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" + child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) def validate_quantity(child_item, d): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fb52c1f6ca..edc40c430a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -144,7 +144,7 @@ class SellingController(StockController): if sales_person.commission_rate: sales_person.incentives = flt( - sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0, + sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0, self.precision("incentives", sales_person)) total += sales_person.allocated_percentage @@ -502,4 +502,4 @@ def set_default_income_account_for_item(obj): for d in obj.get("items"): if d.item_code: if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, 'income_account', d.income_account) \ No newline at end of file + set_item_default(d.item_code, obj.company, 'income_account', d.income_account) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 11ac703311..f352bae30e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -495,7 +495,7 @@ class StockController(AccountsController): "voucher_no": self.name, "company": self.company }) - if check_if_future_sle_exists(args): + if future_sle_exists(args): create_repost_item_valuation_entry(args) elif not is_reposting_pending(): check_if_stock_and_account_balance_synced(self.posting_date, @@ -506,37 +506,42 @@ def is_reposting_pending(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) -def check_if_future_sle_exists(args): - sl_entries = frappe.db.get_all("Stock Ledger Entry", +def future_sle_exists(args): + sl_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], order_by="creation asc") - distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries])) + if not sl_entries: + return - sle_exists = False - for item_code, warehouse in distinct_item_warehouses: - args.update({ - "item_code": item_code, - "warehouse": warehouse - }) - if get_sle(args): - sle_exists = True - break - return sle_exists + warehouse_items_map = {} + for entry in sl_entries: + if entry.warehouse not in warehouse_items_map: + warehouse_items_map[entry.warehouse] = set() + + warehouse_items_map[entry.warehouse].add(entry.item_code) + + or_conditions = [] + for warehouse, items in warehouse_items_map.items(): + or_conditions.append( + "warehouse = '{}' and item_code in ({})".format( + warehouse, + ", ".join(frappe.db.escape(item) for item in items) + ) + ) -def get_sle(args): return frappe.db.sql(""" select name from `tabStock Ledger Entry` where - item_code=%(item_code)s - and warehouse=%(warehouse)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + ({}) + and timestamp(posting_date, posting_time) + >= timestamp(%(posting_date)s, %(posting_time)s) and voucher_no != %(voucher_no)s and is_cancelled = 0 limit 1 - """, args) + """.format(" or ".join(or_conditions)), args) def create_repost_item_valuation_entry(args): args = frappe._dict(args) @@ -554,4 +559,4 @@ def create_repost_item_valuation_entry(args): repost_entry.allow_zero_rate = args.allow_zero_rate repost_entry.flags.ignore_links = True repost_entry.save() - repost_entry.submit() \ No newline at end of file + repost_entry.submit() diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index aab5770a94..e329b325b3 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -113,7 +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")) - item.discount_amount = item.rate_with_margin - item.rate + if not item.discount_amount: + item.discount_amount = item.rate_with_margin - item.rate + elif not item.discount_percentage: + item.rate -= item.discount_amount elif flt(item.price_list_rate) > 0: item.discount_amount = item.price_list_rate - item.rate elif flt(item.price_list_rate) > 0 and not item.discount_amount: @@ -795,7 +798,7 @@ class init_landed_taxes_and_totals(object): for d in self.doc.get(self.tax_field): if d.account_currency == company_currency: d.exchange_rate = 1 - elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: + elif not d.exchange_rate: d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, account_currency=d.account_currency, company=self.doc.company) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 47b05f306b..0522ace1e5 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -248,7 +248,6 @@ def make_quotation(source_name, target_doc=None): "doctype": "Quotation", "field_map": { "opportunity_from": "quotation_to", - "opportunity_type": "order_type", "name": "enq_no", } }, diff --git a/erpnext/education/doctype/student_attendance/student_attendance.json b/erpnext/education/doctype/student_attendance/student_attendance.json index 55384b9e53..e6e46d1c1b 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.json +++ b/erpnext/education/doctype/student_attendance/student_attendance.json @@ -10,6 +10,7 @@ "naming_series", "student", "student_name", + "student_mobile_number", "course_schedule", "student_group", "column_break_3", @@ -93,11 +94,19 @@ "options": "Student Attendance", "print_hide": 1, "read_only": 1 + }, + { + "fetch_from": "student.student_mobile_number", + "fieldname": "student_mobile_number", + "fieldtype": "Read Only", + "label": "Student Mobile Number", + "options": "Phone" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-08 13:55:42.580181", + "modified": "2021-03-24 00:02:11.005895", "modified_by": "Administrator", "module": "Education", "name": "Student Attendance", diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 74ad456ea6..5f471ab2e7 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest, os, json -from frappe.utils import cstr +from frappe.utils import cstr, cint from erpnext.erpnext_integrations.connectors.shopify_connection import create_order from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer @@ -13,9 +13,14 @@ from frappe.core.doctype.data_import.data_import import import_doc class ShopifySettings(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): frappe.set_user("Administrator") + cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')) + if not cls.allow_negative_stock: + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) + # use the fixture data import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) @@ -24,9 +29,15 @@ class ShopifySettings(unittest.TestCase): frappe.reload_doctype("Delivery Note") frappe.reload_doctype("Sales Invoice") - self.setup_shopify() + cls.setup_shopify() - def setup_shopify(self): + @classmethod + def tearDownClass(cls): + if not cls.allow_negative_stock: + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + + @classmethod + def setup_shopify(cls): shopify_settings = frappe.get_doc("Shopify Settings") shopify_settings.taxes = [] @@ -56,21 +67,20 @@ class ShopifySettings(unittest.TestCase): "delivery_note_series": "DN-" }).save(ignore_permissions=True) - self.shopify_settings = shopify_settings + cls.shopify_settings = shopify_settings def test_order(self): - ### Create Customer ### + # Create Customer with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer: shopify_customer = json.load(shopify_customer) create_customer(shopify_customer.get("customer"), self.shopify_settings) - ### Create Item ### + # Create Item with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item: shopify_item = json.load(shopify_item) make_item("_Test Warehouse - _TC", shopify_item.get("product")) - - ### Create Order ### + # Create Order with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: shopify_order = json.load(shopify_order) @@ -80,17 +90,17 @@ class ShopifySettings(unittest.TestCase): self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id) - #check for customer + # Check for customer shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id")) sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id") self.assertEqual(shopify_order_customer_id, sales_order_customer_id) - #check sales invoice + # Check sales invoice sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id}) self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total) - #check delivery note + # Check delivery note delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note` where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0] diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 5ced845c1b..aaf0e855d4 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -53,7 +53,7 @@ "discharge_ordered_date", "discharge_practitioner", "discharge_encounter", - "discharge_date", + "discharge_datetime", "cb_discharge", "discharge_instructions", "followup_date", @@ -404,14 +404,15 @@ "permlevel": 1 }, { - "fieldname": "discharge_date", - "fieldtype": "Date", + "fieldname": "discharge_datetime", + "fieldtype": "Datetime", "label": "Discharge Date", "read_only": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-05-21 02:26:22.144575", + "modified": "2021-03-18 14:44:11.689956", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 88d7f0b233..2934316c06 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -151,7 +151,7 @@ def check_out_inpatient(inpatient_record): def discharge_patient(inpatient_record): validate_inpatient_invoicing(inpatient_record) - inpatient_record.discharge_date = today() + inpatient_record.discharge_datetime = now_datetime() inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4b3597afd7..c2798a36b6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -195,6 +195,10 @@ sounds = [ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, ] +has_upload_permission = { + "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission" +} + has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", "Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", @@ -324,6 +328,7 @@ scheduler_events = { "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", + "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py index 92b1eaee2c..3c42bd9fc3 100644 --- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py @@ -8,6 +8,8 @@ import unittest from frappe.utils import nowdate from datetime import date +test_dependencies = ["Employee"] + class TestAttendanceRequest(unittest.TestCase): def setUp(self): for doctype in ["Attendance Request", "Attendance"]: @@ -56,4 +58,4 @@ class TestAttendanceRequest(unittest.TestCase): self.assertEqual(attendance.docstatus, 2) def get_employee(): - return frappe.get_doc("Employee", "_T-Employee-00001") \ No newline at end of file + return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index 1615ab30f1..74ce30108f 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -10,6 +10,8 @@ from erpnext.hr.doctype.attendance_request.test_attendance_request import get_em from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on +test_dependencies = ["Employee"] + class TestCompensatoryLeaveRequest(unittest.TestCase): def setUp(self): frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') @@ -129,4 +131,4 @@ def create_holiday_list(): ], "holiday_list_name": "_Test Compensatory Leave" }) - holiday_list.save() \ No newline at end of file + holiday_list.save() diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index d0e7d0537b..629bc57118 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -8,7 +8,7 @@ from frappe.utils import getdate, validate_email_address, today, add_years, form from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ - set_user_permission_if_allowed, has_permission + set_user_permission_if_allowed, has_permission, get_doc_permissions from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet @@ -66,7 +66,7 @@ class Employee(NestedSet): def validate_user_details(self): data = frappe.db.get_value('User', self.user_id, ['enabled', 'user_image'], as_dict=1) - if data.get("user_image"): + if data.get("user_image") and self.image == '': self.image = data.get("user_image") self.validate_for_enabled_user_id(data.get("enabled", 0)) self.validate_duplicate_user_id() @@ -501,3 +501,10 @@ def has_user_permission_for_employee(user_name, employee_name): 'allow': 'Employee', 'for_value': employee_name }) + +def has_upload_permission(doc, ptype='read', user=None): + if not user: + user = frappe.session.user + if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): + return True + return doc.user_id == user \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index d8aae66796..09666c5db5 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -13,6 +13,7 @@ "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "leave_settings", + "send_leave_notification", "leave_approval_notification_template", "leave_status_notification_template", "role_allowed_to_create_backdated_leave_application", @@ -69,15 +70,19 @@ "label": "Leave Settings" }, { + "depends_on": "eval: doc.send_leave_notification == 1", "fieldname": "leave_approval_notification_template", "fieldtype": "Link", "label": "Leave Approval Notification Template", + "mandatory_depends_on": "eval: doc.send_leave_notification == 1", "options": "Email Template" }, { + "depends_on": "eval: doc.send_leave_notification == 1", "fieldname": "leave_status_notification_template", "fieldtype": "Link", "label": "Leave Status Notification Template", + "mandatory_depends_on": "eval: doc.send_leave_notification == 1", "options": "Email Template" }, { @@ -132,13 +137,19 @@ "fieldname": "automatically_allocate_leaves_based_on_leave_policy", "fieldtype": "Check", "label": "Automatically Allocate Leaves Based On Leave Policy" + }, + { + "default": "1", + "fieldname": "send_leave_notification", + "fieldtype": "Check", + "label": "Send Leave Notification" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-02-25 12:31:14.947865", + "modified": "2021-03-14 02:04:22.907159", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index 6d275c82d9..872834230e 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -13,11 +13,21 @@ class TestJobApplicant(unittest.TestCase): def create_job_applicant(**args): args = frappe._dict(args) - job_applicant = frappe.get_doc({ - "doctype": "Job Applicant", + + filters = { "applicant_name": args.applicant_name or "_Test Applicant", "email_id": args.email_id or "test_applicant@example.com", + } + + if frappe.db.exists("Job Applicant", filters): + return frappe.get_doc("Job Applicant", filters) + + job_applicant = frappe.get_doc({ + "doctype": "Job Applicant", "status": args.status or "Open" }) + + job_applicant.update(filters) job_applicant.save() - return job_applicant \ No newline at end of file + + return job_applicant diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py index 8886596450..690a692ddc 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.py +++ b/erpnext/hr/doctype/job_offer/test_job_offer.py @@ -13,14 +13,15 @@ from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company class TestJobOffer(unittest.TestCase): def test_job_offer_creation_against_vacancies(self): - create_staffing_plan(staffing_details=[{ - "designation": "Designer", + frappe.db.set_value("HR Settings", None, "check_vacancies", 1) + job_applicant = create_job_applicant(email_id="test_job_offer@example.com") + job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer") + + create_staffing_plan(name='Test No Vacancies', staffing_details=[{ + "designation": "UX Designer", "vacancies": 0, "estimated_cost_per_position": 5000 }]) - frappe.db.set_value("HR Settings", None, "check_vacancies", 1) - job_applicant = create_job_applicant(email_id="test_job_offer@example.com") - job_offer = create_job_offer(job_applicant=job_applicant.name, designation="Researcher") self.assertRaises(frappe.ValidationError, job_offer.submit) # test creation of job offer when vacancies are not present diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 26f077a649..0b71036c86 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -6,6 +6,10 @@ from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation class TestLeaveAllocation(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabLeave Period`") + def test_overlapping_allocation(self): frappe.db.sql("delete from `tabLeave Allocation`") @@ -177,4 +181,4 @@ def create_leave_allocation(**args): }) return leave_allocation -test_dependencies = ["Employee", "Leave Type"] \ No newline at end of file +test_dependencies = ["Employee", "Leave Type"] diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 132c3bd3b9..350ceadccd 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -40,7 +40,8 @@ class LeaveApplication(Document): def on_update(self): if self.status == "Open" and self.docstatus < 1: # notify leave approver about creation - self.notify_leave_approver() + if frappe.db.get_single_value("HR Settings", "send_leave_notification"): + self.notify_leave_approver() def on_submit(self): if self.status == "Open": @@ -50,7 +51,8 @@ class LeaveApplication(Document): self.update_attendance() # notify leave applier about approval - self.notify_employee() + if frappe.db.get_single_value("HR Settings", "send_leave_notification"): + self.notify_employee() self.create_leave_ledger_entry() self.reload() @@ -60,7 +62,8 @@ class LeaveApplication(Document): def on_cancel(self): self.create_leave_ledger_entry(submit=False) # notify leave applier about cancellation - self.notify_employee() + if frappe.db.get_single_value("HR Settings", "send_leave_notification"): + self.notify_employee() self.cancel_attendance() def validate_applicable_after(self): diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 53b7a39e51..b335c48594 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -12,7 +12,7 @@ from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees -test_dependencies = ["Leave Allocation", "Leave Block List"] +test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] _test_records = [ { @@ -639,4 +639,4 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el "docstatus": 1 }).insert() - allocate_leave.submit() \ No newline at end of file + allocate_leave.submit() diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index c7bc6fb775..838e794795 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -9,6 +9,8 @@ from erpnext.hr.doctype.leave_application.test_leave_application import get_leav from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy +test_dependencies = ["Employee"] + class TestLeavePolicyAssignment(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 3dcfcbf4a5..230bb2b0e4 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -7,6 +7,8 @@ import frappe import unittest from frappe.utils import nowdate, add_days +test_dependencies = ["Shift Type"] + class TestShiftRequest(unittest.TestCase): def setUp(self): for doctype in ["Shift Request", "Shift Assignment"]: @@ -46,4 +48,4 @@ 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() \ No newline at end of file + department_doc.reload() diff --git a/erpnext/hr/doctype/shift_type/test_records.json b/erpnext/hr/doctype/shift_type/test_records.json new file mode 100644 index 0000000000..9040b915a1 --- /dev/null +++ b/erpnext/hr/doctype/shift_type/test_records.json @@ -0,0 +1,8 @@ +[ + { + "doctype": "Shift Type", + "name": "Day Shift", + "start_time": "9:00:00", + "end_time": "18:00:00" + } +] diff --git a/erpnext/hr/doctype/shift_type/test_shift_type.py b/erpnext/hr/doctype/shift_type/test_shift_type.py index 535072a035..bc4f0eafcd 100644 --- a/erpnext/hr/doctype/shift_type/test_shift_type.py +++ b/erpnext/hr/doctype/shift_type/test_shift_type.py @@ -7,14 +7,4 @@ import frappe import unittest class TestShiftType(unittest.TestCase): - def test_make_shift_type(self): - if frappe.db.exists("Shift Type", "Day Shift"): - return - shift_type = frappe.get_doc({ - "doctype": "Shift Type", - "name": "Day Shift", - "start_time": "9:00:00", - "end_time": "18:00:00" - }) - shift_type.insert() - \ No newline at end of file + pass diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 5b84d00bd6..533149a823 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -39,6 +39,7 @@ class StaffingPlan(Document): detail.current_count = designation_counts['employee_count'] detail.current_openings = designation_counts['job_openings'] + detail.total_estimated_cost = 0 if detail.number_of_positions > 0: if detail.vacancies > 0 and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py index e961114ac2..f5fece8049 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py @@ -31,7 +31,7 @@ def get_columns(): "fieldtype": "Link", "fieldname": "job_opening", "options": "Job Opening", - "width": 100 + "width": 105 }, { "label": _("Job Applicant"), @@ -44,13 +44,13 @@ def get_columns(): "label": _("Applicant name"), "fieldtype": "data", "fieldname": "applicant_name", - "width": 120 + "width": 130 }, { "label": _("Application Status"), "fieldtype": "Data", "fieldname": "application_status", - "width": 100 + "width": 150 }, { "label": _("Job Offer"), diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 13a209418d..4b9a89486a 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -275,6 +275,11 @@ class TestLoan(unittest.TestCase): frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 where loan_security='Test Security 2'""") + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertEquals(loan_security_shortfall.status, "Completed") + self.assertEquals(loan_security_shortfall.shortfall_amount, 0) + def test_loan_security_unpledge(self): pledge = [{ "loan_security": "Test Security 1", 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 6469806884..b5e78981d0 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 @@ -55,6 +55,9 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): 'total_interest_payable', 'disbursed_amount', 'status'], filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) + loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall", + fields=["loan", "name"], filters={"status": "Pending"}, as_list=1)) + loan_security_map = {} for loan in loans: @@ -71,14 +74,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): for security, qty in pledged_securities.items(): if not ltv_ratio: ltv_ratio = get_ltv_ratio(security) - security_value += loan_security_price_map.get(security) * qty + security_value += flt(loan_security_price_map.get(security)) * flt(qty) - current_ratio = (outstanding_amount/security_value) * 100 + current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0 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) + 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): existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") @@ -101,3 +109,11 @@ def get_ltv_ratio(loan_security): ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio') return ltv_ratio +def update_pending_shortfall(shortfall): + # Get all pending loan security shortfall + frappe.db.set_value("Loan Security Shortfall", shortfall, + { + "status": "Completed", + "shortfall_amount": 0 + }) + diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 662a06b1ee..7aaf2a08ec 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -255,6 +255,9 @@ class JobCard(Document): data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None data.actual_end_time = time_data[0].end_time if time_data else None + if data.get("workstation") != self.workstation: + # workstations can change in a job card + data.workstation = self.workstation wo.flags.ignore_validate_update_after_submit = True wo.update_operation_status() diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index f93b244a50..6c60bbde86 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -11,10 +11,14 @@ "from_warehouse", "warehouse", "column_break_4", + "required_bom_qty", "quantity", "uom", "projected_qty", "actual_qty", + "ordered_qty", + "reserved_qty_for_production", + "safety_stock", "item_details", "description", "min_order_qty", @@ -129,11 +133,40 @@ "fieldtype": "Link", "label": "From Warehouse", "options": "Warehouse" + }, + { + "fetch_from": "item_code.safety_stock", + "fieldname": "safety_stock", + "fieldtype": "Float", + "label": "Safety Stock", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "label": "Reserved Qty for Production", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "required_bom_qty", + "fieldtype": "Float", + "label": "Required Qty as per BOM", + "no_copy": 1, + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-02-03 12:22:29.913302", + "modified": "2021-03-26 12:41:13.013149", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index b723387a09..15ec6209c1 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -251,7 +251,8 @@ frappe.ui.form.on('Production Plan', { get_items_for_material_requests: function(frm, warehouses) { const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', - 'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type']; + 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty', + 'reserved_qty_for_production', 'material_request_type']; frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 7daf7069f3..f11470086a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -32,6 +32,7 @@ "material_request_planning", "include_non_stock_items", "include_subcontracted_items", + "include_safety_stock", "ignore_existing_ordered_qty", "column_break_25", "for_warehouse", @@ -309,13 +310,19 @@ "fieldtype": "Select", "label": "Sales Order Status", "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver" + }, + { + "default": "0", + "fieldname": "include_safety_stock", + "fieldtype": "Check", + "label": "Include Safety Stock in Required Qty Calculation" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-10 18:01:54.991970", + "modified": "2021-03-08 11:17:25.470147", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8f9dd05217..109c8b5647 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -434,12 +434,14 @@ def download_raw_materials(doc): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse', - 'projected Qty', 'Actual Qty']] + item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', + 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', + 'Safety Stock', 'Required Qty']] for d in get_items_for_material_requests(doc): - item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'), - d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')]) + item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), + d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), + d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -447,8 +449,9 @@ def download_raw_materials(doc): if d.get("warehouse") == bin_dict.get('warehouse'): continue - item_list.append(['', '', '', '', bin_dict.get('warehouse'), - bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0)]) + item_list.append(['', '', '', bin_dict.get('warehouse'), '', + bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0), + bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)]) build_csv_response(item_list, doc.name) @@ -482,7 +485,7 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, + bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock, item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor FROM `tabBOM Item` bom_item @@ -518,8 +521,8 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite include_non_stock_items, include_subcontracted_items, d.qty) return item_details -def get_material_request_items(row, sales_order, - company, ignore_existing_ordered_qty, warehouse, bin_dict): +def get_material_request_items(row, sales_order, company, + ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict): total_qty = row['qty'] required_qty = 0 @@ -543,17 +546,24 @@ def get_material_request_items(row, sales_order, if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): required_qty = ceil(required_qty) + if include_safety_stock: + required_qty += flt(row['safety_stock']) + if required_qty > 0: return { 'item_code': row.item_code, 'item_name': row.item_name, 'quantity': required_qty, + 'required_bom_qty': total_qty, 'description': row.description, 'stock_uom': row.get("stock_uom"), 'warehouse': warehouse or row.get('source_warehouse') \ or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), + 'safety_stock': row.safety_stock, 'actual_qty': bin_dict.get("actual_qty", 0), 'projected_qty': bin_dict.get("projected_qty", 0), + 'ordered_qty': bin_dict.get("ordered_qty", 0), + 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0), 'min_order_qty': row['min_order_qty'], 'material_request_type': row.get("default_material_request_type"), 'sales_order': sales_order, @@ -620,7 +630,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): """.format(lft, rgt, company) return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, - ifnull(sum(actual_qty),0) as actual_qty, warehouse from `tabBin` + ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) @@ -660,6 +671,7 @@ def get_items_for_material_requests(doc, warehouses=None): company = doc.get('company') ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') + include_safety_stock = doc.get('include_safety_stock') so_item_details = frappe._dict() for data in po_items: @@ -711,6 +723,7 @@ def get_items_for_material_requests(doc, warehouses=None): 'description' : item_master.description, 'stock_uom' : item_master.stock_uom, 'conversion_factor' : conversion_factor, + 'safety_stock': item_master.safety_stock } ) @@ -732,7 +745,7 @@ def get_items_for_material_requests(doc, warehouses=None): if details.qty > 0: items = get_material_request_items(details, sales_order, company, - ignore_existing_ordered_qty, warehouse, bin_dict) + ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict) if items: mr_items.append(items) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 00e8c5418a..08291d1eae 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -82,7 +82,7 @@ class TestWorkOrder(unittest.TestCase): wo_order.set_work_order_operations() self.assertEqual(wo_order.planned_operating_cost, cost*2) - def test_resered_qty_for_partial_completion(self): + def test_reserved_qty_for_partial_completion(self): item = "_Test Item" warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC") @@ -109,7 +109,7 @@ class TestWorkOrder(unittest.TestCase): s.submit() bin1_at_completion = get_bin(item, warehouse) - + self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1) @@ -592,6 +592,55 @@ class TestWorkOrder(unittest.TestCase): frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") + def test_make_stock_entry_for_customer_provided_item(self): + finished_item = 'Test Item for Make Stock Entry 1' + make_item(finished_item, { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + }) + + customer_provided_item = 'CUST-0987' + make_item(customer_provided_item, { + 'is_purchase_item': 0, + 'is_customer_provided_item': 1, + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + 'customer': '_Test Customer' + }) + + if not frappe.db.exists('BOM', {'item': finished_item}): + make_bom(item=finished_item, raw_materials=[customer_provided_item], rm_qty=1) + + company = "_Test Company with perpetual inventory" + customer_warehouse = create_warehouse("Test Customer Provided Warehouse", company=company) + wo = make_wo_order_test_record(item=finished_item, qty=1, source_warehouse=customer_warehouse, + company=company) + + ste = frappe.get_doc(make_stock_entry(wo.name, purpose='Material Transfer for Manufacture')) + ste.insert() + + self.assertEqual(len(ste.items), 1) + for item in ste.items: + self.assertEqual(item.allow_zero_valuation_rate, 1) + self.assertEqual(item.valuation_rate, 0) + + def test_valuation_rate_missing_on_make_stock_entry(self): + item_name = 'Test Valuation Rate Missing' + make_item(item_name, { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }) + + if not frappe.db.get_value('BOM', {'item': item_name}): + make_bom(item=item_name, raw_materials=[item_name], rm_qty=1) + + company = "_Test Company with perpetual inventory" + source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company) + wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, + company=company) + + self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` @@ -609,6 +658,15 @@ def allow_overproduction(fieldname, percentage): def make_wo_order_test_record(**args): args = frappe._dict(args) + if args.company and args.company != "_Test Company": + warehouse_map = { + "fg_warehouse": "_Test FG Warehouse", + "wip_warehouse": "_Test WIP Warehouse" + } + + for attr, wh_name in warehouse_map.items(): + if not args.get(attr): + args[attr] = create_warehouse(wh_name, company=args.company) wo_order = frappe.new_doc("Work Order") wo_order.production_item = args.production_item or args.item or args.item_code or "_Test FG Item" diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 585a09db2b..cd9edeeea8 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -333,8 +333,7 @@ "fieldname": "operations", "fieldtype": "Table", "label": "Operations", - "options": "Work Order Operation", - "read_only": 1 + "options": "Work Order Operation" }, { "depends_on": "operations", @@ -496,7 +495,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-05 19:32:43.323054", + "modified": "2021-03-16 13:27:51.116484", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 2ca9f1694b..fc27d35598 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -61,7 +61,7 @@ class ForecastingReport(ExponentialSmoothingForecast): from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1) self.period_list = get_period_list(from_date, self.filters.to_date, - from_date, self.filters.to_date, None, self.filters.periodicity, ignore_fiscal_year=True) + from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True) order_data = self.get_data_for_forecast() or [] diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index e947588482..4fd1a30ab9 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -42,7 +42,7 @@ class Donation(Document): self.load_from_db() self.create_payment_entry() - def create_payment_entry(self): + def create_payment_entry(self, date=None): settings = frappe.get_doc('Non Profit Settings') if not settings.automate_donation_payment_entries: return @@ -58,8 +58,9 @@ class Donation(Document): frappe.flags.ignore_account_permission = False pe.paid_from = settings.donation_debit_account pe.paid_to = settings.donation_payment_account + pe.posting_date = date or getdate() pe.reference_no = self.name - pe.reference_date = getdate() + pe.reference_date = date or getdate() pe.flags.ignore_mandatory = True pe.insert() pe.submit() @@ -91,6 +92,10 @@ def capture_razorpay_donations(*args, **kwargs): if not data.event == 'payment.captured': return + # to avoid capturing subscription payments as donations + if payment.description and 'subscription' in str(payment.description).lower(): + return + donor = get_donor(payment.email) if not donor: donor = create_donor(payment) @@ -119,7 +124,7 @@ def create_donation(donor, payment): 'donor_name': donor.donor_name, 'email': donor.email, 'date': getdate(), - 'amount': flt(payment.amount), + 'amount': flt(payment.amount) / 100, # Convert to rupees from paise 'mode_of_payment': payment.method, 'razorpay_payment_id': payment.id }).insert(ignore_mandatory=True) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 191281f4ce..52447e4386 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -48,7 +48,7 @@ class Membership(Document): last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership - if last_membership and not frappe.session.user == "Administrator": + if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator": # if last membership does not expire in 30 days, then do not allow to renew if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : frappe.throw(_("You can only renew if your membership expires within 30 days")) @@ -90,6 +90,7 @@ class Membership(Document): self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) + self.reload() self.invoice = invoice.name if with_payment_entry: @@ -284,10 +285,11 @@ def trigger_razorpay_subscription(*args, **kwargs): settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: + membership.reload() membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) except Exception as e: - message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) + message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) notify_failure(log) return { "status": "Failed", "reason": e} diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js index cff92b42ab..4c4ca9834b 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Non Profit Settings", { }; }); - frm.set_query("debit_account", function() { + frm.set_query("membership_debit_account", function() { return { filters: { "account_type": "Receivable", @@ -29,6 +29,16 @@ frappe.ui.form.on("Non Profit Settings", { }; }); + frm.set_query("donation_debit_account", function() { + return { + filters: { + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.donation_company + } + }; + }); + frm.set_query("membership_payment_account", function () { var account_types = ["Bank", "Cash"]; return { @@ -40,6 +50,17 @@ frappe.ui.form.on("Non Profit Settings", { }; }); + frm.set_query("donation_payment_account", function () { + var account_types = ["Bank", "Cash"]; + return { + filters: { + "account_type": ["in", account_types], + "is_group": 0, + "company": frm.doc.donation_company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 59b12f319e..46f0d4ae79 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -720,7 +720,7 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes -erpnext.patches.v13_0.add_standard_navbar_items #4 +erpnext.patches.v13_0.add_standard_navbar_items #2021-03-24 erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail @@ -759,3 +759,6 @@ erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae +erpnext.patches.v13_0.setup_uae_vat_fields +execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') +erpnext.patches.v13_0.rename_discharge_date_in_ip_record diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py index d41101cc46..29a7b4bd60 100644 --- a/erpnext/patches/v12_0/add_state_code_for_ladakh.py +++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py @@ -11,6 +11,7 @@ def execute(): # Update options in gst_state custom fields for field in custom_fields: - gst_state_field = frappe.get_doc('Custom Field', field) - gst_state_field.options = '\n'.join(states) - gst_state_field.save() + if frappe.db.exists('Custom Field', field): + gst_state_field = frappe.get_doc('Custom Field', field) + gst_state_field.options = '\n'.join(states) + gst_state_field.save() diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py new file mode 100644 index 0000000000..491dc82f78 --- /dev/null +++ b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc("Healthcare", "doctype", "Inpatient Record") + if frappe.db.has_column("Inpatient Record", "discharge_date"): + rename_field("Inpatient Record", "discharge_date", "discharge_datetime") diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py index aea53f8add..833c355d5f 100644 --- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py +++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py @@ -2,15 +2,12 @@ import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + if frappe.get_all('Company', filters = {'country': 'India'}): + make_custom_fields() - make_custom_fields() - - if not frappe.db.exists('Party Type', 'Donor'): - frappe.get_doc({ - 'doctype': 'Party Type', - 'party_type': 'Donor', - 'account_type': 'Receivable' - }).insert(ignore_permissions=True) \ No newline at end of file + if not frappe.db.exists('Party Type', 'Donor'): + frappe.get_doc({ + 'doctype': 'Party Type', + 'party_type': 'Donor', + 'account_type': 'Receivable' + }).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py new file mode 100644 index 0000000000..d7a5c682df --- /dev/null +++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py @@ -0,0 +1,12 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from erpnext.regional.united_arab_emirates.setup import setup + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'}) + if not company: + return + + setup() \ No newline at end of file diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index f5af677fce..13b6c05e22 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -9,17 +9,10 @@ from frappe import _, bold from frappe.utils import getdate, date_diff, comma_and, formatdate class AdditionalSalary(Document): - def on_submit(self): if self.ref_doctype == "Employee Advance" and self.ref_docname: frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount) - def before_insert(self): - if frappe.db.exists("Additional Salary", {"employee": self.employee, "salary_component": self.salary_component, - "amount": self.amount, "payroll_date": self.payroll_date, "company": self.company, "docstatus": 1}): - - frappe.throw(_("Additional Salary Component Exists.")) - def validate(self): self.validate_dates() self.validate_salary_structure() @@ -89,10 +82,11 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days -@frappe.whitelist() -def get_additional_salary_component(employee, start_date, end_date, component_type): - additional_salaries = frappe.db.sql(""" - select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date +def get_additional_salaries(employee, start_date, end_date, component_type): + additional_salary_list = frappe.db.sql(""" + select name, salary_component as component, type, amount, + overwrite_salary_structure_amount as overwrite, + deduct_full_tax_on_selected_payroll_date from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 @@ -102,7 +96,7 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty from_date <= %(to_date)s and to_date >= %(to_date)s ) and type = %(component_type)s - order by salary_component, overwrite_salary_structure_amount DESC + order by salary_component, overwrite ASC """, { 'employee': employee, 'from_date': start_date, @@ -110,38 +104,18 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty 'component_type': "Earning" if component_type == "earnings" else "Deduction" }, as_dict=1) - existing_salary_components= [] - salary_components_details = {} - additional_salary_details = [] + additional_salaries = [] + components_to_overwrite = [] - overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1] + for d in additional_salary_list: + if d.overwrite: + if d.component in components_to_overwrite: + frappe.throw(_("Multiple Additional Salaries with overwrite " + "property exist for Salary Component {0} between {1} and {2}.").format( + frappe.bold(d.component), start_date, end_date), title=_("Error")) - component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type'] - for d in additional_salaries: + components_to_overwrite.append(d.component) - if d.salary_component not in existing_salary_components: - component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) - struct_row = frappe._dict({'salary_component': d.salary_component}) - if component: - struct_row.update(component[0]) + additional_salaries.append(d) - struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date - struct_row['is_additional_component'] = 1 - - salary_components_details[d.salary_component] = struct_row - - - if overwrites_components.count(d.salary_component) > 1: - frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error")) - else: - additional_salary_details.append({ - 'name': d.name, - 'component': d.salary_component, - 'amount': d.amount, - 'type': d.type, - 'overwrite': d.overwrite_salary_structure_amount, - }) - - existing_salary_components.append(d.salary_component) - - return salary_components_details, additional_salary_details + return additional_salaries diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index e89e3dd077..7daea2da47 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -15,9 +15,12 @@ from frappe.utils import getdate, add_days, get_datetime, flt test_dependencies = ["Salary Component", "Salary Slip", "Account"] class TestGratuity(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) + + def setUp(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index e098ec79b0..84c381489c 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -12,7 +12,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment -from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry +from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans class TestPayrollEntry(unittest.TestCase): @@ -168,15 +168,23 @@ class TestPayrollEntry(unittest.TestCase): salary_structure = "Test Salary Structure for Loan" make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) + if not frappe.db.exists("Loan Type", "Car Loan"): + create_loan_accounts() + create_loan_type("Car Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - dates = get_start_end_dates('Monthly', nowdate()) make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") @@ -267,4 +275,4 @@ def get_salary_slip(user, period, salary_structure): salary_slip.calculate_net_pay() salary_slip.db_update() - return salary_slip \ No newline at end of file + return salary_slip diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 7460c75227..d5278393a1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -74,43 +74,46 @@ frappe.ui.form.on("Salary Slip", { if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } + }, + + currency: function(frm) { frm.trigger("set_dynamic_labels"); }, set_dynamic_labels: function(frm) { var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency"); - frappe.run_serially([ - () => frm.events.set_exchange_rate(frm, company_currency), - () => frm.events.change_form_labels(frm, company_currency), - () => frm.events.change_grid_labels(frm), - () => frm.refresh_fields() - ]); + if (frm.doc.employee && frm.doc.currency) { + frappe.run_serially([ + () => frm.events.set_exchange_rate(frm, company_currency), + () => frm.events.change_form_labels(frm, company_currency), + () => frm.events.change_grid_labels(frm), + () => frm.refresh_fields() + ]); + } }, set_exchange_rate: function(frm, company_currency) { - 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) { - 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.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", ""); } } }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 595d6974fd..a04a635807 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -13,7 +13,7 @@ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_da from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.utilities.transaction_base import TransactionBase from frappe.utils.background_jobs import enqueue -from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salary_component +from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits @@ -524,7 +524,7 @@ class SalarySlip(TransactionBase): except NameError as err: frappe.throw(_("{0}