From 7a5937a98c3d74bdb1da79c3d5a8f49ef76ded2d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 Jan 2022 17:53:15 +0530 Subject: [PATCH 01/94] fix(India): Tax and Charges template not getting fetched based on tax category assigned --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5b260c252d1..2287714a008 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -217,7 +217,7 @@ def get_regional_address_details(party_details, doctype, company): if tax_template_by_category: party_details['taxes_and_charges'] = tax_template_by_category - return + return party_details if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details From 342658ea7028cc0d890200364cc08cf97e55d6d0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 Jan 2022 18:39:59 +0530 Subject: [PATCH 02/94] fix: Test Case --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 480d09fb0b3..d01baebe636 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1213,7 +1213,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') + fiscal_year = get_fiscal_year(date=nowdate()) if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), From 4fdaf51599bf45aeca31b3221fcf0e3752f9bac4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 11:41:23 +0530 Subject: [PATCH 03/94] fix: filter query in bank reconciliation tool (#29099) --- .../bank_reconciliation_tool/bank_reconciliation_tool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index f7d471b725e..dd7409f4b01 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { frm.set_query("bank_account", function () { return { filters: { - company: ["in", frm.doc.company], + company: frm.doc.company, 'is_company_account': 1 }, }; From 674a923669d821231cf70d5b093a8568fdde5555 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 13:42:25 +0530 Subject: [PATCH 04/94] feat: Add currency in import download statement (#29093) --- .../doctype/bank_statement_import/bank_statement_import.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 016f29a7b51..866633f74e5 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", { "withdrawal", "description", "reference_number", - "bank_account" + "bank_account", + "currency" ], }, }); From fcbcb08fcdf2afd6869d2157a4b6fa1abecf8a5f Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 3 Jan 2022 14:28:34 +0530 Subject: [PATCH 05/94] fix: incorrect posting time fetching incorrect qty (#29103) (cherry picked from commit f02e6b463188a35b0798cdd6174f374b24bbc81b) --- erpnext/stock/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index dd87e4ff499..4a8c97fb10a 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -87,8 +87,8 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None from erpnext.stock.stock_ledger import get_previous_sle - if not posting_date: posting_date = nowdate() - if not posting_time: posting_time = nowtime() + if posting_date is None: posting_date = nowdate() + if posting_time is None: posting_time = nowtime() args = { "item_code": item_code, From 0222cd198d48634877cda4fbe3ae107d12eae6f9 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 15 Dec 2021 22:34:44 +0530 Subject: [PATCH 06/94] fix: Validation in POS for item batch no stock quantity (cherry picked from commit 9f235526d43658e333bc20bf8847bb8d21764332) --- .../doctype/pos_invoice/pos_invoice.py | 22 +++++++++- .../doctype/pos_invoice/test_pos_invoice.py | 43 ++++++++++++++++++- erpnext/stock/doctype/batch/batch.py | 26 +++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 814372f6b35..dffcbb7c0d4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account +from erpnext.stock.doctype.batch.batch import get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos @@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) elif invalid_serial_nos: - frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") + frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + def validate_pos_reserved_batch_qty(self, item): + filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} + + available_batch_qty = frappe.db.get_value('Batch', item.batch_no, 'batch_qty') + reserved_batch_qty = get_pos_reserved_batch_qty(filters) + + bold_item_name = frappe.bold(item.item_name) + bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty)) + bold_invalid_batch_no = frappe.bold(item.batch_no) + + if (available_batch_qty - reserved_batch_qty) == 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") + .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) + elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") + .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) + def validate_delivered_serial_nos(self, item): serial_nos = get_serial_nos(item.serial_no) delivered_serial_nos = frappe.db.get_list('Serial No', { @@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + elif d.batch_no: + self.validate_pos_reserved_batch_qty(d) else: if allow_negative_stock: return diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 66963335376..666e3f3e402 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -521,6 +521,41 @@ class TestPOSInvoice(unittest.TestCase): rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) + def test_pos_batch_ietm_qty_validation(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01') + item = frappe.get_doc('Item', '_BATCH ITEM') + batch = frappe.get_doc('Batch', 'TestBatch 01') + batch.submit() + item.batch_no = 'TestBatch 01' + item.save() + + se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01') + + pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) + pos_inv1.items[0].batch_no = 'TestBatch 01' + pos_inv1.save() + pos_inv1.submit() + + pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) + pos_inv2.items[0].batch_no = 'TestBatch 01' + pos_inv2.save() + + self.assertRaises(frappe.ValidationError, pos_inv2.submit) + + #teardown + pos_inv1.reload() + pos_inv1.cancel() + pos_inv1.delete() + pos_inv2.reload() + pos_inv2.delete() + se.cancel() + batch.reload() + batch.cancel() + batch.delete() + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None @@ -557,7 +592,8 @@ def create_pos_invoice(**args): "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no + "serial_no": args.serial_no, + "batch_no": args.batch_no }) if not args.do_not_save: @@ -570,3 +606,8 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv + +def make_batch_item(item_name): + from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): + return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3ce2d87f711..a3ba7e8d91f 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -313,3 +313,29 @@ def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" frappe.get_doc(args).insert().name + +@frappe.whitelist() +def get_pos_reserved_batch_qty(filters): + import json + + if isinstance(filters, str): + filters = json.loads(filters) + + pos_transacted_batch_nos = frappe.db.sql("""select item.qty + from `tabPOS Invoice` p, `tabPOS Invoice Item` item + where p.name = item.parent + and p.consolidated_invoice is NULL + and p.status != "Consolidated" + and p.docstatus = 1 + and item.docstatus = 1 + and item.item_code = %(item_code)s + and item.warehouse = %(warehouse)s + and item.batch_no = %(batch_no)s + + """, filters, as_dict=1) + + reserved_batch_qty = 0.0 + for d in pos_transacted_batch_nos: + reserved_batch_qty += d.qty + + return reserved_batch_qty \ No newline at end of file From 3947d859c5e514a37fde770da1c673148cbd461f Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Thu, 16 Dec 2021 13:26:21 +0530 Subject: [PATCH 07/94] fix: replaced sql query with frappe.qb (cherry picked from commit 68beee2a00dba2b08ca3265f509f60d636b2dcb8) --- erpnext/stock/doctype/batch/batch.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index a3ba7e8d91f..b91320f67be 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -321,18 +321,19 @@ def get_pos_reserved_batch_qty(filters): if isinstance(filters, str): filters = json.loads(filters) - pos_transacted_batch_nos = frappe.db.sql("""select item.qty - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.status != "Consolidated" - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.batch_no = %(batch_no)s + p = frappe.qb.DocType("POS Invoice").as_("p") + item = frappe.qb.DocType("POS Invoice Item").as_("item") - """, filters, as_dict=1) + pos_transacted_batch_nos = frappe.qb.from_(p).from_(item).select(item.qty).where( + (p.name == item.parent) & + (p.consolidated_invoice.isnull()) & + (p.status != "Consolidated") & + (p.docstatus == 1) & + (item.docstatus == 1) & + (item.item_code == filters.get('item_code')) & + (item.warehouse == filters.get('warehouse')) & + (item.batch_no == filters.get('batch_no')) + ).run(as_dict=True) reserved_batch_qty = 0.0 for d in pos_transacted_batch_nos: From bf92681fff397d01d318506e5f9ff69870049231 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 21 Dec 2021 17:01:10 +0530 Subject: [PATCH 08/94] fix: typo (cherry picked from commit 9c8f5235374d19a03a8f7f03e0ca73a7040568b7) --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 666e3f3e402..7d31e0aa195 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -521,7 +521,7 @@ class TestPOSInvoice(unittest.TestCase): rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) - def test_pos_batch_ietm_qty_validation(self): + def test_pos_batch_item_qty_validation(self): from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_batch_item_with_batch, ) From 0be144c1932540c95d3a7804e65c31a4c6e2f395 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 21 Dec 2021 17:05:36 +0530 Subject: [PATCH 09/94] chore: remove extra whitespace (cherry picked from commit c3a9ca09d15893ca48e405617a9a5bc215ae92ba) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index dffcbb7c0d4..0aeaae0dbf4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -143,7 +143,7 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: - frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") + frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) def validate_delivered_serial_nos(self, item): From 80f92ee547f90221b0599a579d13242a435b925c Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 16:40:11 +0530 Subject: [PATCH 10/94] fix: added Sum() in query (cherry picked from commit 267da4888900c7c2b6796637dc19d68ff442f07f) --- erpnext/stock/doctype/batch/batch.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b91320f67be..1df645dc69f 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -323,8 +323,9 @@ def get_pos_reserved_batch_qty(filters): p = frappe.qb.DocType("POS Invoice").as_("p") item = frappe.qb.DocType("POS Invoice Item").as_("item") + sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") - pos_transacted_batch_nos = frappe.qb.from_(p).from_(item).select(item.qty).where( + reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( (p.name == item.parent) & (p.consolidated_invoice.isnull()) & (p.status != "Consolidated") & @@ -333,10 +334,6 @@ def get_pos_reserved_batch_qty(filters): (item.item_code == filters.get('item_code')) & (item.warehouse == filters.get('warehouse')) & (item.batch_no == filters.get('batch_no')) - ).run(as_dict=True) + ).run() - reserved_batch_qty = 0.0 - for d in pos_transacted_batch_nos: - reserved_batch_qty += d.qty - - return reserved_batch_qty \ No newline at end of file + return reserved_batch_qty[0][0] \ No newline at end of file From 110a1e5557ed8101405922012e063b7f51e589f8 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 16:57:00 +0530 Subject: [PATCH 11/94] fix: using get_batch_qty method to get available_qty (cherry picked from commit 73854972198e99ba529651211ad264e9c50ea5ab) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 4 ++-- erpnext/stock/doctype/batch/batch.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0aeaae0dbf4..d5961223358 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,7 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.stock.doctype.batch.batch import get_pos_reserved_batch_qty +from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos @@ -132,7 +132,7 @@ class POSInvoice(SalesInvoice): def validate_pos_reserved_batch_qty(self, item): filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} - available_batch_qty = frappe.db.get_value('Batch', item.batch_no, 'batch_qty') + available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) reserved_batch_qty = get_pos_reserved_batch_qty(filters) bold_item_name = frappe.bold(item.item_name) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1df645dc69f..3b01e7484cf 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -336,4 +336,5 @@ def get_pos_reserved_batch_qty(filters): (item.batch_no == filters.get('batch_no')) ).run() - return reserved_batch_qty[0][0] \ No newline at end of file + flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) + return flt_reserved_batch_qty \ No newline at end of file From 1efce21ba4ed7b9d65d4d9829f23ebda58c7773b Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Sat, 30 Oct 2021 16:44:15 +0530 Subject: [PATCH 12/94] refactor: update_invoice_status with query builder (cherry picked from commit 6c96ed4e11b17acf46d5ea9bba4f1df60cd3238a) --- erpnext/controllers/accounts_controller.py | 104 +++++++++++---------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 28cded2e671..ac78a524c5f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder.functions import Sum from frappe.utils import ( add_days, add_months, @@ -1692,58 +1693,63 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, def update_invoice_status(): """Updates status as Overdue for applicable invoices. Runs daily.""" today = getdate() - + payment_schedule = frappe.qb.DocType("Payment Schedule") for doctype in ("Sales Invoice", "Purchase Invoice"): - frappe.db.sql(""" - UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue' - WHERE invoice.docstatus = 1 - AND invoice.status REGEXP '^Unpaid|^Partly Paid' - AND invoice.outstanding_amount > 0 - AND ( - {or_condition} - ( - ( - CASE - WHEN invoice.party_account_currency = invoice.currency - THEN ( - CASE - WHEN invoice.disable_rounded_total - THEN invoice.grand_total - ELSE invoice.rounded_total - END - ) - ELSE ( - CASE - WHEN invoice.disable_rounded_total - THEN invoice.base_grand_total - ELSE invoice.base_rounded_total - END - ) - END - ) - invoice.outstanding_amount - ) < ( - SELECT SUM( - CASE - WHEN invoice.party_account_currency = invoice.currency - THEN ps.payment_amount - ELSE ps.base_payment_amount - END - ) - FROM `tabPayment Schedule` ps - WHERE ps.parent = invoice.name - AND ps.due_date < %(today)s - ) - ) - """.format( - doctype=doctype, - or_condition=( - "invoice.is_pos AND invoice.due_date < %(today)s OR" - if doctype == "Sales Invoice" - else "" - ) - ), {"today": today} + invoice = frappe.qb.DocType(doctype) + + consider_base_amount = invoice.party_account_currency != invoice.currency + payment_amount = ( + frappe.qb.terms.Case() + .when(consider_base_amount, payment_schedule.base_payment_amount) + .else_(payment_schedule.payment_amount) ) + payable_amount = ( + frappe.qb.from_(payment_schedule) + .select(Sum(payment_amount)) + .where( + (payment_schedule.parent == invoice.name) + & (payment_schedule.due_date < today) + ) + ) + + total = ( + frappe.qb.terms.Case() + .when(invoice.disable_rounded_total, invoice.grand_total) + .else_(invoice.rounded_total) + ) + + base_total = ( + frappe.qb.terms.Case() + .when(invoice.disable_rounded_total, invoice.base_grand_total) + .else_(invoice.base_rounded_total) + ) + + total_amount = ( + frappe.qb.terms.Case() + .when(consider_base_amount, base_total) + .else_(total) + ) + + is_overdue = total_amount - invoice.outstanding_amount < payable_amount + + conditions = ( + (invoice.docstatus == 1) + & (invoice.outstanding_amount > 0) + & ( + invoice.status.like('Unpaid%') + | invoice.status.like('Partly Paid%') + ) + & ( + (invoice.is_pos & invoice.due_date < today) | is_overdue + if doctype == "Sales Invoice" + else is_overdue + ) + ) + + frappe.qb.update(invoice).set("status", "Overdue").where(conditions).run() + + @frappe.whitelist() def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None): if not terms_template: From e648a2ccde106f18e66f4bb680c49a47ca4664d1 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Fri, 12 Nov 2021 12:56:29 +0530 Subject: [PATCH 13/94] fix: consider `Discounted` status (cherry picked from commit 0799f378b4e29d376b1e3cdbe3edc132cf15009e) --- erpnext/controllers/accounts_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ac78a524c5f..3cc73402a85 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1737,8 +1737,8 @@ def update_invoice_status(): (invoice.docstatus == 1) & (invoice.outstanding_amount > 0) & ( - invoice.status.like('Unpaid%') - | invoice.status.like('Partly Paid%') + invoice.status.like("Unpaid%") + | invoice.status.like("Partly Paid%") ) & ( (invoice.is_pos & invoice.due_date < today) | is_overdue @@ -1747,7 +1747,13 @@ def update_invoice_status(): ) ) - frappe.qb.update(invoice).set("status", "Overdue").where(conditions).run() + status = ( + frappe.qb.terms.Case() + .when(invoice.status.like("%Discounted"), "Overdue and Discounted") + .else_("Overdue") + ) + + frappe.qb.update(invoice).set("status", status).where(conditions).run() @frappe.whitelist() From 50628d416ce29511b8acb9d72fa4c940fa93dc9f Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Mon, 20 Dec 2021 13:23:14 +0530 Subject: [PATCH 14/94] test: update_invoice_status (cherry picked from commit 4d48ca42282c28ab682dbcec0391b9770630df3a) --- .../sales_invoice/test_sales_invoice.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6696d4ad2a8..e2c1f79bd2e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.regional.india.utils import get_ewb_data @@ -2358,6 +2359,41 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_update_invoice_status(self): + today = nowdate() + + # Sales Invoice without Payment Schedule + si = create_sales_invoice(posting_date=add_days(today, -5)) + + # Sales Invoice with Payment Schedule + si_with_payment_schedule = create_sales_invoice(do_not_submit=True) + si_with_payment_schedule.extend("payment_schedule", [ + { + "due_date": add_days(today, -5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2 + }, + { + "due_date": add_days(today, 5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2 + } + ]) + si_with_payment_schedule.submit() + + + for invoice in (si, si_with_payment_schedule): + invoice.db_set("status", "Unpaid") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue") + + invoice.db_set("status", "Unpaid and Discounted") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue and Discounted") + + def test_sales_commission(self): si = frappe.copy_doc(test_records[0]) item = copy.deepcopy(si.get('items')[0]) From 84f3f34821a2dcc1fbecc7f3e63c294abf2ca1b9 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Mon, 20 Dec 2021 13:24:19 +0530 Subject: [PATCH 15/94] Update erpnext/controllers/accounts_controller.py Co-authored-by: Saqib (cherry picked from commit 16a90d3e606cfed1e0ce499ece6b223162451dc6) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3cc73402a85..6e57a5bfac6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1741,7 +1741,7 @@ def update_invoice_status(): | invoice.status.like("Partly Paid%") ) & ( - (invoice.is_pos & invoice.due_date < today) | is_overdue + ((invoice.is_pos & invoice.due_date < today) | is_overdue) if doctype == "Sales Invoice" else is_overdue ) From a34ded79f3482b53e243ce0b7cf714381b5417e2 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:01:57 +0530 Subject: [PATCH 16/94] fix: importing Sum() for qb for test fix --- erpnext/stock/doctype/batch/batch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3b01e7484cf..b32033c771b 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -316,6 +316,7 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): + from frappe.query_builder.functions import Sum import json if isinstance(filters, str): @@ -323,7 +324,7 @@ def get_pos_reserved_batch_qty(filters): p = frappe.qb.DocType("POS Invoice").as_("p") item = frappe.qb.DocType("POS Invoice Item").as_("item") - sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") + sum_qty = Sum(item.qty).as_("qty") reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( (p.name == item.parent) & @@ -337,4 +338,4 @@ def get_pos_reserved_batch_qty(filters): ).run() flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) - return flt_reserved_batch_qty \ No newline at end of file + return flt_reserved_batch_qty From 860a0f469613e60b4e417925d0e021771d69f1e4 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:06:40 +0530 Subject: [PATCH 17/94] fix: linters fix --- erpnext/stock/doctype/batch/batch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index b32033c771b..a6f07f8237b 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -316,9 +316,9 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): - from frappe.query_builder.functions import Sum import json - + from frappe.query_builder.functions import Sum + if isinstance(filters, str): filters = json.loads(filters) From fe3ebafea444a612fb8975f848e2db3ab6cae600 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 19:10:03 +0530 Subject: [PATCH 18/94] fix: linters fix --- erpnext/stock/doctype/batch/batch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index a6f07f8237b..2fcdba59e35 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -317,8 +317,9 @@ def make_batch(args): @frappe.whitelist() def get_pos_reserved_batch_qty(filters): import json + from frappe.query_builder.functions import Sum - + if isinstance(filters, str): filters = json.loads(filters) From fe1a66cf5442ea9e3be9e9b643ccfd584a7e291e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 11:58:41 +0530 Subject: [PATCH 19/94] fix: future recurring period calculation (backport #29083) (#29118) Co-authored-by: Rucha Mahabal (cherry picked from commit 11cfa1615864ecabce230858cb4cc48a77d7db2a) Co-authored-by: Nabin Hait --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index efbc4681d7a..8b82b3a4af4 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -936,8 +936,11 @@ class SalarySlip(TransactionBase): def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount): future_recurring_additional_amount = 0 to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date') + # future month count excluding current - future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month) + from_date, to_date = getdate(self.start_date), getdate(to_date) + future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month) + if future_recurring_period > 0: future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month return future_recurring_additional_amount @@ -1036,7 +1039,8 @@ class SalarySlip(TransactionBase): data.update({"annual_taxable_earning": annual_taxable_earning}) tax_amount = 0 for slab in tax_slab.slabs: - if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): + cond = cstr(slab.condition).strip() + if cond and not self.eval_tax_slab_condition(cond, data): continue if not slab.to_amount and annual_taxable_earning >= slab.from_amount: tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 From 1d270dca05501b5a15d19e3270188029b48eafde Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 4 Jan 2022 17:53:08 +0530 Subject: [PATCH 20/94] fix: Show GL balance in Accounts Receivable and payable summary --- .../accounts_receivable_summary.js | 5 +++++ .../accounts_receivable_summary.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 305cddb102a..715cd6476e8 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Show Future Payments"), "fieldtype": "Check", }, + { + "fieldname":"show_gl_balance", + "label": __("Show GL Balance"), + "fieldtype": "Check", + }, ], onload: function(report) { diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index a95bcf83ef7..4559fa94a4a 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import cint +from frappe.utils import cint, flt from six import iteritems from erpnext.accounts.party import get_partywise_advanced_payment_amount @@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} + if self.filters.show_gl_balance: + gl_balance_map = get_gl_balance(self.filters.report_date) + for party, party_dict in iteritems(self.party_total): if party_dict.outstanding == 0: continue @@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): # but in summary report advance shown in separate column row.paid -= row.advance + if self.filters.show_gl_balance: + row.gl_balance = gl_balance_map.get(party) + row.diff = flt(row.outstanding) - flt(row.gl_balance) + self.data.append(row) def get_party_total(self, args): @@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(_(credit_debit_label), fieldname='credit_note') self.add_column(_('Outstanding Amount'), fieldname='outstanding') + if self.filters.show_gl_balance: + self.add_column(_('GL Balance'), fieldname='gl_balance') + self.add_column(_('Difference'), fieldname='diff') + self.setup_ageing_columns() if self.party_type == "Customer": @@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): # Add column for total due amount self.add_column(label="Total Amount Due", fieldname='total_due') + +def get_gl_balance(report_date): + return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'], + filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1)) From 2838dc537e1b42d4351fa9753190e424f14162a4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:05:38 +0530 Subject: [PATCH 21/94] fix: update idx after updating items in so/po (#29134) (#29136) (cherry picked from commit 733c9defdfe96c3fa1f75d29c9ec085cf69aced6) Co-authored-by: Ankush Menat --- erpnext/controllers/accounts_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6e57a5bfac6..1da8affb285 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2125,6 +2125,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_status_updater() else: parent.check_credit_limit() + + # reset index of child table + for idx, row in enumerate(parent.get(child_docname), start=1): + row.idx = idx + parent.save() if parent_doctype == 'Purchase Order': From f4670a86a0008febb8aaf7e3c5fe131ec80cee1f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:16:52 +0530 Subject: [PATCH 22/94] fix: minor issues (#29140) --- erpnext/accounts/doctype/account/account.js | 26 ++++++++++--------- .../uae_vat_settings/uae_vat_settings.js | 12 ++++++--- .../page/point_of_sale/pos_item_selector.js | 2 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 7a1d7359488..320e1cab7c3 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -43,12 +43,12 @@ frappe.ui.form.on('Account', { frm.trigger('add_toolbar_buttons'); } if (frm.has_perm('write')) { - frm.add_custom_button(__('Update Account Name / Number'), function () { - frm.trigger("update_account_number"); - }); frm.add_custom_button(__('Merge Account'), function () { frm.trigger("merge_account"); - }); + }, __('Actions')); + frm.add_custom_button(__('Update Account Name / Number'), function () { + frm.trigger("update_account_number"); + }, __('Actions')); } } }, @@ -59,11 +59,12 @@ frappe.ui.form.on('Account', { } }, add_toolbar_buttons: function(frm) { - frm.add_custom_button(__('Chart of Accounts'), - function () { frappe.set_route("Tree", "Account"); }); + frm.add_custom_button(__('Chart of Accounts'), () => { + frappe.set_route("Tree", "Account"); + }, __('View')); if (frm.doc.is_group == 1) { - frm.add_custom_button(__('Group to Non-Group'), function () { + frm.add_custom_button(__('Convert to Non-Group'), function () { return frappe.call({ doc: frm.doc, method: 'convert_group_to_ledger', @@ -71,10 +72,11 @@ frappe.ui.form.on('Account', { frm.refresh(); } }); - }); + }, __('Actions')); + } else if (cint(frm.doc.is_group) == 0 && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { - frm.add_custom_button(__('Ledger'), function () { + frm.add_custom_button(__('General Ledger'), function () { frappe.route_options = { "account": frm.doc.name, "from_date": frappe.sys_defaults.year_start_date, @@ -82,9 +84,9 @@ frappe.ui.form.on('Account', { "company": frm.doc.company }; frappe.set_route("query-report", "General Ledger"); - }); + }, __('View')); - frm.add_custom_button(__('Non-Group to Group'), function () { + frm.add_custom_button(__('Convert to Group'), function () { return frappe.call({ doc: frm.doc, method: 'convert_ledger_to_group', @@ -92,7 +94,7 @@ frappe.ui.form.on('Account', { frm.refresh(); } }); - }); + }, __('Actions')); } }, diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js index 07a93010b51..66531412faf 100644 --- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js @@ -2,7 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('UAE VAT Settings', { - // refresh: function(frm) { - - // } + onload: function(frm) { + frm.set_query('account', 'uae_vat_accounts', function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); + } }); diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 496385248c4..a30bcd7cf6d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -113,7 +113,7 @@ erpnext.PointOfSale.ItemSelector = class { `
${get_item_image_html()} From 06c0313a266245347f7086888deb0e419d176436 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:20:19 +0530 Subject: [PATCH 23/94] fix: POS items added to cart despite low qty (#29141) --- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index e61a634aaee..ce74f6d0a58 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -643,7 +643,7 @@ erpnext.PointOfSale.Controller = class { message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) }) } else if (available_qty < qty_needed) { - frappe.show_alert({ + frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); From b808ef1625591be5c11dc1351e45507f0958fd0e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:29:39 +0530 Subject: [PATCH 24/94] feat: allow user to change the parent company (#29138) --- erpnext/setup/doctype/company/company.js | 11 ++-- erpnext/setup/doctype/company/company.py | 14 ++++- erpnext/setup/doctype/company/test_company.py | 55 +++++++++++++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 91f60fbd4e2..45e8dccc319 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -79,14 +79,11 @@ frappe.ui.form.on("Company", { }, refresh: function(frm) { - if(!frm.doc.__islocal) { - frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1); - frm.set_df_property("parent_company", "read_only", 1); - disbale_coa_fields(frm); - } + frm.toggle_display('address_html', !frm.is_new()); - frm.toggle_display('address_html', !frm.doc.__islocal); - if(!frm.doc.__islocal) { + if (!frm.is_new()) { + frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1); + disbale_coa_fields(frm); frappe.contacts.render_address_and_contact(frm); frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'} diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 955bfb41392..142fe04b6f2 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -49,6 +49,7 @@ class Company(NestedSet): self.validate_perpetual_inventory() self.validate_perpetual_inventory_for_non_stock_items() self.check_country_change() + self.check_parent_changed() self.set_chart_of_accounts() self.validate_parent_company() @@ -132,6 +133,10 @@ class Company(NestedSet): self.name in frappe.local.enable_perpetual_inventory: frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory + if frappe.flags.parent_company_changed: + from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Company", "parent_company") + frappe.clear_cache() def create_default_warehouses(self): @@ -193,7 +198,7 @@ class Company(NestedSet): def check_country_change(self): frappe.flags.country_change = False - if not self.get('__islocal') and \ + if not self.is_new() and \ self.country != frappe.get_cached_value('Company', self.name, 'country'): frappe.flags.country_change = True @@ -398,6 +403,13 @@ class Company(NestedSet): if not frappe.db.get_value('GL Entry', {'company': self.name}): frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name) + def check_parent_changed(self): + frappe.flags.parent_company_changed = False + + if not self.is_new() and \ + self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"): + frappe.flags.parent_company_changed = True + def get_name_with_abbr(name, company): company_abbr = frappe.get_cached_value('Company', company, "abbr") parts = name.split(" - ") diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index 4ee94927381..e175c5435aa 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -93,6 +93,61 @@ class TestCompany(unittest.TestCase): frappe.db.sql(""" delete from `tabMode of Payment Account` where company =%s """, (company)) + def test_basic_tree(self, records=None): + min_lft = 1 + max_rgt = frappe.db.sql("select max(rgt) from `tabCompany`")[0][0] + + if not records: + records = test_records[2:] + + for company in records: + lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"], + ["lft", "rgt", "parent_company"]) + + if parent_company: + parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, + ["lft", "rgt"]) + else: + # root + parent_lft = min_lft - 1 + parent_rgt = max_rgt + 1 + + self.assertTrue(lft) + self.assertTrue(rgt) + self.assertTrue(lft < rgt) + self.assertTrue(parent_lft < parent_rgt) + self.assertTrue(lft > parent_lft) + self.assertTrue(rgt < parent_rgt) + self.assertTrue(lft >= min_lft) + self.assertTrue(rgt <= max_rgt) + + def get_no_of_children(self, company): + def get_no_of_children(companies, no_of_children): + children = [] + for company in companies: + children += frappe.db.sql_list("""select name from `tabCompany` + where ifnull(parent_company, '')=%s""", company or '') + + if len(children): + return get_no_of_children(children, no_of_children + len(children)) + else: + return no_of_children + + return get_no_of_children([company], 0) + + def test_change_parent_company(self): + child_company = frappe.get_doc("Company", "_Test Company 5") + + # changing parent of company + child_company.parent_company = "_Test Company 3" + child_company.save() + self.test_basic_tree() + + # move it back + child_company.parent_company = "_Test Company 4" + child_company.save() + self.test_basic_tree() + def create_company_communication(doctype, docname): comm = frappe.get_doc({ "doctype": "Communication", From fa735a05396dbd39f4f54e9160582e9c9f2cec17 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 20:04:47 +0530 Subject: [PATCH 25/94] fix: Modifying Opening invoice creation tool timestamp (#29127) (#29143) --- .../opening_invoice_creation_tool.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index bc9241802da..daee8f8c1ab 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -75,7 +75,7 @@ ], "hide_toolbar": 1, "issingle": 1, - "modified": "2019-07-25 14:57:33.187689", + "modified": "2022-01-04 15:25:06.053187", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", From 5900968bd9905d0cdb86f939f6a052fd670e6a1b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 4 Jan 2022 19:21:16 +0530 Subject: [PATCH 26/94] fix: map Accounting Dimensions for Bank Entry against Payroll Entry (cherry picked from commit 5eeb94db313f805c5e740198bfa0d0fa008c3718) --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 84c59a2c2b8..e3ddaf93988 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -350,23 +350,24 @@ class PayrollEntry(Document): currencies = [] multi_currency = 0 company_currency = erpnext.get_company_currency(self.company) + accounting_dimensions = get_accounting_dimensions() or [] exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": self.payment_account, "bank_account": self.bank_account, "credit_in_account_currency": flt(amount, precision), "exchange_rate": flt(exchange_rate), - }) + }, accounting_dimensions)) exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": payroll_payable_account, "debit_in_account_currency": flt(amount, precision), "exchange_rate": flt(exchange_rate), "reference_type": self.doctype, "reference_name": self.name - }) + }, accounting_dimensions)) if len(currencies) > 1: multi_currency = 1 From 485fa4700fed88424d51a23c0b3a0db1906c400f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 4 Jan 2022 21:52:42 +0530 Subject: [PATCH 27/94] fix(UX): validate setup on clicking Mark Attendance button in Shift Type (backport #29146) (#29148) (cherry picked from commit ac816f4fed0a557c821fabb1a0d9e803d7e367ae) Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/shift_type/shift_type.js | 33 ++++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js index ba53312bcef..7138e3bcf30 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.js +++ b/erpnext/hr/doctype/shift_type/shift_type.js @@ -4,15 +4,32 @@ frappe.ui.form.on('Shift Type', { refresh: function(frm) { frm.add_custom_button( - 'Mark Attendance', - () => frm.call({ - doc: frm.doc, - method: 'process_auto_attendance', - freeze: true, - callback: () => { - frappe.msgprint(__("Attendance has been marked as per employee check-ins")); + __('Mark Attendance'), + () => { + if (!frm.doc.enable_auto_attendance) { + frm.scroll_to_field('enable_auto_attendance'); + frappe.throw(__('Please Enable Auto Attendance and complete the setup first.')); } - }) + + if (!frm.doc.process_attendance_after) { + frm.scroll_to_field('process_attendance_after'); + frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()])); + } + + if (!frm.doc.last_sync_of_checkin) { + frm.scroll_to_field('last_sync_of_checkin'); + frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()])); + } + + frm.call({ + doc: frm.doc, + method: 'process_auto_attendance', + freeze: true, + callback: () => { + frappe.msgprint(__('Attendance has been marked as per employee check-ins')); + } + }); + } ); } }); From a64efb764b61b2c452da48f62a2a894464bccf97 Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Wed, 5 Jan 2022 11:56:32 +0530 Subject: [PATCH 28/94] refactor: Add deprecation warning for agriculture domain (#29149) --- erpnext/patches.txt | 1 + .../patches/v13_0/agriculture_deprecation_warning.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 erpnext/patches/v13_0/agriculture_deprecation_warning.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c69ac090f65..67f41d0678b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,3 +339,4 @@ erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template +erpnext.patches.v13_0.agriculture_deprecation_warning diff --git a/erpnext/patches/v13_0/agriculture_deprecation_warning.py b/erpnext/patches/v13_0/agriculture_deprecation_warning.py new file mode 100644 index 00000000000..09ccfb3ea47 --- /dev/null +++ b/erpnext/patches/v13_0/agriculture_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture", + fg="yellow", + ) From 1d87e9d8f67e3611993d638f2d422fe66acf2c00 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 5 Jan 2022 15:08:29 +0530 Subject: [PATCH 29/94] fix: Currency in KSA VAT report --- erpnext/regional/report/ksa_vat/ksa_vat.py | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index b41b2b0428f..cc26bd7a57a 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -20,25 +20,35 @@ def get_columns(): "fieldname": "title", "label": _("Title"), "fieldtype": "Data", - "width": 300 + "width": 300, }, { "fieldname": "amount", "label": _("Amount (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, }, { "fieldname": "adjustment_amount", "label": _("Adjustment (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, }, { "fieldname": "vat_amount", "label": _("VAT Amount (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Currency", + "width": 150, + "hidden": 1 } ] @@ -47,6 +57,8 @@ def get_data(filters): # Validate if vat settings exist company = filters.get('company') + company_currency = frappe.get_cached_value('Company', company, "default_currency") + if frappe.db.exists('KSA VAT Setting', company) is None: url = get_url_to_list('KSA VAT Setting') frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) @@ -55,7 +67,7 @@ def get_data(filters): ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company) # Sales Heading - append_data(data, 'VAT on Sales', '', '', '') + append_data(data, 'VAT on Sales', '', '', '', company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 @@ -67,7 +79,7 @@ def get_data(filters): # Adding results to data append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax) + total_taxable_adjustment_amount, total_tax, company_currency) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount @@ -75,13 +87,13 @@ def get_data(filters): # Sales Grand Total append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax) + grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) # Blank Line - append_data(data, '', '', '', '') + append_data(data, '', '', '', '', company_currency) # Purchase Heading - append_data(data, 'VAT on Purchases', '', '', '') + append_data(data, 'VAT on Purchases', '', '', '', company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 @@ -93,7 +105,7 @@ def get_data(filters): # Adding results to data append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax) + total_taxable_adjustment_amount, total_tax, company_currency) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount @@ -101,7 +113,7 @@ def get_data(filters): # Purchase Grand Total append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax) + grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) return data @@ -147,9 +159,10 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): -def append_data(data, title, amount, adjustment_amount, vat_amount): +def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): """Returns data with appended value.""" - data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount}) + data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount, + "currency": company_currency}) def get_tax_amount(item_code, account_head, doctype, parent): if doctype == 'Sales Invoice': From 6cc3c612eebbc73bb63550f0140d2c3f699b70c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 5 Jan 2022 16:00:00 +0530 Subject: [PATCH 30/94] feat: 'Invoice Number' field in Opening Invoice Creation Tool (#29156) --- .../opening_invoice_creation_tool.py | 8 +++++-- .../test_opening_invoice_creation_tool.py | 24 +++++++++++++++---- .../opening_invoice_creation_tool_item.json | 4 +++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 222589180ee..a8d7bf7a0e7 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document): frappe.scrub(row.party_type): row.party, "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0 + "update_stock": 0, + "invoice_number": row.invoice_number }) accounting_dimension = get_accounting_dimensions() @@ -200,10 +201,13 @@ def start_import(invoices): names = [] for idx, d in enumerate(invoices): try: + invoice_number = None + if d.invoice_number: + invoice_number = d.invoice_number publish(idx, len(invoices), d.doctype) doc = frappe.get_doc(d) doc.flags.ignore_mandatory = True - doc.insert() + doc.insert(set_name=invoice_number) doc.submit() frappe.db.commit() names.append(doc.name) 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 2da7425f9de..b5aae9845b6 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 @@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() - def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None): + def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None): doc = frappe.get_single("Opening Invoice Creation Tool") args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, - party_1=party_1, party_2=party_2) + party_1=party_1, party_2=party_2, invoice_number=invoice_number) doc.update(args) return doc.make_invoices() @@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): # teardown frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) + def test_renaming_of_invoice_using_invoice_number_field(self): + company = "_Test Opening Invoice Company" + party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") + self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11") + + sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name") + sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name") + self.assertEqual(sales_inv1, "TEST-NEW-INV-11") + + #teardown + for inv in [sales_inv1, sales_inv2]: + doc = frappe.get_doc('Sales Invoice', inv) + doc.cancel() + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") @@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args): "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company) + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": args.get("invoice_number") }, { "qty": 2.0, @@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args): "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company) + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": None } ] }) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 07c78631358..8149d594cb4 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -1,9 +1,11 @@ { + "actions": [], "creation": "2017-08-29 04:26:36.159247", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "invoice_number", "party_type", "party", "temporary_opening_account", @@ -114,7 +116,7 @@ ], "istable": 1, "links": [], - "modified": "2021-12-17 19:25:06.053187", + "modified": "2022-01-04 18:40:15.927675", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", From 37d2f6fd15e3932e5d461815d9be636cf30bd0ad Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 6 Jan 2022 11:44:58 +0530 Subject: [PATCH 31/94] fix: Earned Leave allocation from Leave Policy Assignment (backport #29163) (#29165) * fix: Earned Leave allocation from Leave Policy Assignment * test: Earned Leave Allocation from Leave Policy Assignment (cherry picked from commit 26247be3b872a28f425ce51af663eeb0e0899799) Co-authored-by: Rucha Mahabal --- .../leave_policy_assignment.py | 2 + .../test_leave_policy_assignment.py | 61 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index a0e1d19420c..c79216a275d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -129,6 +129,8 @@ class LeavePolicyAssignment(Document): monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding) new_leaves_allocated = monthly_earned_leave * months_passed + else: + new_leaves_allocated = 0 return new_leaves_allocated 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 b1861ad4d8e..8953a51e8bb 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 @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.utils import add_months, get_first_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( test_dependencies = ["Employee"] class TestLeavePolicyAssignment(unittest.TestCase): - def setUp(self): - for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec def test_grant_leaves(self): @@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase): self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type") - self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date) - self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date) + self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date)) + self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date)) self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name) self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0]) @@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase): # User are now allowed to grant leave self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + def test_earned_leave_allocation(self): + leave_period = create_leave_period("Test Earned Leave Period") + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] + }).insert() + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 0) + def tearDown(self): - for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: - frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + frappe.db.rollback() + + +def create_earned_leave_type(leave_type): + frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) + + return frappe.get_doc(dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6 + )).insert() + + +def create_leave_period(name): + frappe.delete_doc_if_exists("Leave Period", name, force=1) + start_date = get_first_day(getdate()) + + return frappe.get_doc(dict( + name=name, + doctype="Leave Period", + from_date=start_date, + to_date=add_months(start_date, 12), + company="_Test Company", + is_active=1 + )).insert() \ No newline at end of file From b82d4b757707ed13188e3cb30b413484483b571e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 4 Jan 2022 14:43:02 +0530 Subject: [PATCH 32/94] fix: cannot create reverse journal entry (cherry picked from commit 3aa1817f7bec944ba30409b03430eb6ae5da67b5) --- .../accounts/doctype/journal_entry/journal_entry.js | 2 +- .../accounts/doctype/journal_entry/journal_entry.json | 11 ++++++++++- .../accounts/doctype/journal_entry/journal_entry.py | 10 ++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d76641dc9bd..a83ea65541c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", { if(frm.doc.docstatus==1) { frm.add_custom_button(__('Reverse Journal Entry'), function() { return erpnext.journal_entry.reverse_journal_entry(frm); - }, __('Make')); + }, __('Actions')); } if (frm.doc.__islocal) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 20678d787b4..335fd350def 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "reversal_of", "tax_withholding_category", "column_break1", "from_template", @@ -515,13 +516,21 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount " + }, + { + "depends_on": "eval:doc.docstatus", + "fieldname": "reversal_of", + "fieldtype": "Link", + "label": "Reversal Of", + "options": "Journal Entry", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2021-09-09 15:31:14.484029", + "modified": "2022-01-04 13:39:36.485954", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 3aed3c89d53..6ec2d4bda03 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1147,9 +1147,8 @@ def make_inter_company_journal_entry(name, voucher_type, company): def make_reverse_journal_entry(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc - def update_accounts(source, target, source_parent): - target.reference_type = "Journal Entry" - target.reference_name = source_parent.name + def post_process(source, target): + target.reversal_of = source.name doclist = get_mapped_doc("Journal Entry", source_name, { "Journal Entry": { @@ -1167,9 +1166,8 @@ def make_reverse_journal_entry(source_name, target_doc=None): "debit": "credit", "credit_in_account_currency": "debit_in_account_currency", "credit": "debit", - }, - "postprocess": update_accounts, + } }, - }, target_doc) + }, target_doc, post_process) return doclist From 7abe7c752b3e786a95d54582a8862903f71e1d97 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 6 Jan 2022 17:05:00 +0530 Subject: [PATCH 33/94] fix: incorrect serial no valuation report showing cancelled entries (#29172) (#29173) (cherry picked from commit 24fc487dd8c2516908d658a52153328175ec8918) Co-authored-by: Ankush Menat --- .../incorrect_serial_no_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index 5f03c7cbe0a..e4ea9947166 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -74,7 +74,7 @@ def get_stock_ledger_entries(report_filters): fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] - filters = {'serial_no': ("is", "set")} + filters = {'serial_no': ("is", "set"), "is_cancelled": 0} if report_filters.get('item_code'): filters['item_code'] = report_filters.get('item_code') From 60ddf39e3778681d870b8296a401e90396abfa11 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Fri, 7 Jan 2022 09:22:36 +0530 Subject: [PATCH 34/94] chore: Remove the extra 's' in patient age string (#29180) --- erpnext/healthcare/doctype/patient/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 89f146576bb..46d63003cce 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -143,7 +143,7 @@ class Patient(Document): age = self.age if not age: return - age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") + age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") return age_str @frappe.whitelist() From 892e63575e968e23c9e5c033da5bf53ae2704bf3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 6 Jan 2022 13:19:17 +0530 Subject: [PATCH 35/94] fix: GL Entries for loan repayment via Salary (cherry picked from commit f316aaf41e40e41be52c7b10c4bf53651f42f063) --- .../loan_repayment/loan_repayment.json | 23 +++- .../doctype/loan_repayment/loan_repayment.py | 116 +++++++++--------- .../doctype/salary_slip/salary_slip.py | 22 +++- .../doctype/salary_slip/test_salary_slip.py | 2 +- 4 files changed, 99 insertions(+), 64 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 6479853246f..93ef2170420 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -13,8 +13,10 @@ "column_break_3", "company", "posting_date", - "is_term_loan", "rate_of_interest", + "payroll_payable_account", + "is_term_loan", + "repay_from_salary", "payment_details_section", "due_date", "pending_principal_amount", @@ -243,15 +245,31 @@ "label": "Total Penalty Paid", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "depends_on": "eval:doc.repay_from_salary", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "mandatory_depends_on": "eval:doc.repay_from_salary", + "options": "Account" + }, + { + "default": "0", + "fetch_from": "against_loan.repay_from_salary", + "fieldname": "repay_from_salary", + "fieldtype": "Check", + "label": "Repay From Salary" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:10:00.935364", + "modified": "2022-01-06 01:51:06.707782", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -287,5 +305,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index ceaaecb93a0..65099da49b4 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -321,74 +321,79 @@ class LoanRepayment(AccountsController): else: remarks = _("Repayment against Loan: ") + self.against_loan - if not loan_details.repay_from_salary: - if self.total_penalty_paid: - gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.penalty_income_account, - "against": loan_details.payment_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) + if self.repay_from_salary: + payment_account = self.payroll_payable_account + else: + payment_account = loan_details.payment_account + if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, "against": loan_details.payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": remarks, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": payment_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) ) - if gle_map: - make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) + gle_map.append( + self.get_gl_dict({ + "account": payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) def create_repayment_entry(loan, applicant, company, posting_date, loan_type, - payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): + payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None, + payroll_payable_account=None): lr = frappe.get_doc({ "doctype": "Loan Repayment", @@ -401,7 +406,8 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type, "interest_payable": interest_payable, "payable_principal_amount": payable_principal_amount, "amount_paid": amount_paid, - "loan_type": loan_type + "loan_type": loan_type, + "payroll_payable_account": payroll_payable_account }).insert() return lr diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 8b82b3a4af4..8ea791f620f 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1146,15 +1146,17 @@ class SalarySlip(TransactionBase): }) def make_loan_repayment_entry(self): + payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: - repayment_entry = create_repayment_entry(loan.loan, self.employee, - self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, - loan.principal_amount, loan.total_payment) + if loan.total_payment: + repayment_entry = create_repayment_entry(loan.loan, self.employee, + self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, + loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account) - repayment_entry.save() - repayment_entry.submit() + repayment_entry.save() + repayment_entry.submit() - frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) + frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) def cancel_loan_repayment_entry(self): for loan in self.loans: @@ -1388,3 +1390,11 @@ def get_salary_component_data(component): ], as_dict=1, ) + +def get_payroll_payable_account(company, payroll_entry): + if payroll_entry: + payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account') + else: + payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account') + + return payroll_payable_account \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 6227863365b..0977561c9e6 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -384,7 +384,7 @@ class TestSalarySlip(unittest.TestCase): make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', payroll_period=payroll_period) - frappe.db.sql("delete from tabLoan") + frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() From b561c185027062f109d97db418d276d07f7c7275 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 6 Jan 2022 18:58:49 +0530 Subject: [PATCH 36/94] fix: Inconsistency in calculating outstanding amount (cherry picked from commit 9d3a5c3184ccab1500bdbb31bb14cc5b4f56dd30) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- .../doctype/purchase_invoice/purchase_invoice.py | 8 ++++---- .../accounts/doctype/sales_invoice/sales_invoice.py | 10 +++++----- erpnext/controllers/taxes_and_totals.py | 7 ++++--- erpnext/public/js/controllers/taxes_and_totals.js | 13 ++++++++++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 33fbd748c7b..c5cc05749a7 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, self.precision("base_grand_total")) if grand_total and not self.is_internal_transfer(): # Did not use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) gl_entries.append( self.get_gl_dict({ "account": self.credit_to, @@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController): "party": self.supplier, "due_date": self.due_date, "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total \ if self.party_account_currency==self.company_currency else grand_total, "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 176d47897d6..772e8c4e872 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -879,11 +879,11 @@ class SalesInvoice(SellingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, self.precision("base_grand_total")) + if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( self.get_gl_dict({ "account": self.debit_to, @@ -891,8 +891,8 @@ class SalesInvoice(SellingController): "party": self.customer, "due_date": self.due_date, "against": self.against_income_account, - "debit": grand_total_in_company_currency, - "debit_in_account_currency": grand_total_in_company_currency \ + "debit": base_grand_total, + "debit_in_account_currency": base_grand_total \ if self.party_account_currency==self.company_currency else grand_total, "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b9c95508152..9b635cfe914 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -594,13 +594,14 @@ class calculate_taxes_and_totals(object): if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: grand_total = self.doc.rounded_total or self.doc.grand_total + base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total + if self.doc.party_account_currency == self.doc.currency: total_amount_to_pay = flt(grand_total - self.doc.total_advance - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) else: - total_amount_to_pay = flt(flt(grand_total * - self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance - - flt(self.doc.base_write_off_amount), self.doc.precision("grand_total")) + total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance + - flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total")) self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 864c0957d1e..bbf269e9b4d 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -708,14 +708,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) { - var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount), precision("grand_total")); } else { var total_amount_to_pay = flt( - (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + (flt(base_grand_total, precision("base_grand_total")) - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), precision("base_grand_total") ); @@ -745,15 +746,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, +<<<<<<< HEAD set_total_amount_to_default_mop: function() { var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; +======= + set_total_amount_to_default_mop() { + let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; +>>>>>>> 9d3a5c3184 (fix: Inconsistency in calculating outstanding amount) if(this.frm.doc.party_account_currency == this.frm.doc.currency) { var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount), precision("grand_total")); } else { var total_amount_to_pay = flt( - (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + (flt(base_grand_total, precision("base_grand_total")) - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), precision("base_grand_total") ); From 9b6c2fdfb7e33bc2dd716c5f55ae489a04e0ea2f Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:03:55 +0530 Subject: [PATCH 37/94] fix: Conflicts --- erpnext/public/js/controllers/taxes_and_totals.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index bbf269e9b4d..913109a4ac9 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -746,14 +746,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, -<<<<<<< HEAD set_total_amount_to_default_mop: function() { - var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; -======= - set_total_amount_to_default_mop() { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; ->>>>>>> 9d3a5c3184 (fix: Inconsistency in calculating outstanding amount) if(this.frm.doc.party_account_currency == this.frm.doc.currency) { var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance From 4ef9d289e89387fdee660cb7b43161c64730c215 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 22 Dec 2021 13:23:42 +0530 Subject: [PATCH 38/94] fix(sales-invoice): cannot create debit note with zero qty (cherry picked from commit 04ea42ce5267c7ae9d5d286631d938c106116ede) --- erpnext/controllers/taxes_and_totals.py | 2 ++ erpnext/public/js/controllers/taxes_and_totals.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b9c95508152..f0983103cd4 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object): if not item.qty and self.doc.get("is_return"): item.amount = flt(-1 * item.rate, item.precision("amount")) + elif not item.qty and self.doc.get("is_debit_note"): + item.amount = flt(item.rate, item.precision("amount")) else: item.amount = flt(item.rate * item.qty, item.precision("amount")) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 864c0957d1e..07e251efa22 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if ((!item.qty) && me.frm.doc.is_return) { item.amount = flt(item.rate * -1, precision("amount", item)); + } else if ((!item.qty) && me.frm.doc.is_debit_note) { + item.amount = flt(item.rate, precision("amount", item)); } else { item.amount = flt(item.rate * item.qty, precision("amount", item)); } From ecf341d1b1eea20d073b6a0e41c1966a05c3d086 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 23 Dec 2021 11:52:10 +0530 Subject: [PATCH 39/94] fix: cannot save debit note with zero quantity (cherry picked from commit 2be5104848d367fc7e916ab9c3c10d7146338cd6) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1da8affb285..2a95553fbf2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -114,7 +114,7 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1) def validate(self): - if not self.get('is_return'): + if not self.get('is_return') and not self.get('is_debit_note'): self.validate_qty_is_not_zero() if self.get("_action") and self._action != "update_after_submit": From 822a8ba47ab206efda1e97e06973e1a9736bd87b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 7 Jan 2022 19:04:19 +0530 Subject: [PATCH 40/94] chore: remove framework patch for custom fields (#29117) (#29195) * chore: patches were breaking during migration * fix: rewrote the query in query builder * chore: removed patch from patches.txt (cherry picked from commit 25c875e4476e01faa80f9f8195db5138e18b664c) Co-authored-by: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> --- erpnext/patches.txt | 1 - .../v13_0/validate_options_for_data_field.py | 26 ------------------- 2 files changed, 27 deletions(-) delete mode 100644 erpnext/patches/v13_0/validate_options_for_data_field.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 67f41d0678b..35ad56a5193 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -178,7 +178,6 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign -erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py deleted file mode 100644 index ad777b8586d..00000000000 --- a/erpnext/patches/v13_0/validate_options_for_data_field.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2021, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe.model import data_field_options - - -def execute(): - - for field in frappe.get_all('Custom Field', - fields = ['name'], - filters = { - 'fieldtype': 'Data', - 'options': ['!=', None] - }): - - if field not in data_field_options: - frappe.db.sql(""" - UPDATE - `tabCustom Field` - SET - options=NULL - WHERE - name=%s - """, (field)) From ac4e9969280afe756e8fb4f6256c2cfb7bd8caff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 10:49:29 +0530 Subject: [PATCH 41/94] fix: Task Depends on not removed from Gantt chart (backport #28309) (#29207) Co-authored-by: conncampbell (cherry picked from commit 31123436fbe223b0116922097108fa444e5092d3) Co-authored-by: Conn Campbell --- erpnext/projects/doctype/task/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index c8657b29da5..6d3f20f271a 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -105,7 +105,7 @@ class Task(NestedSet): frappe.throw(_("Completed On cannot be greater than Today")) def update_depends_on(self): - depends_on_tasks = self.depends_on_tasks or "" + depends_on_tasks = "" for d in self.depends_on: if d.task and d.task not in depends_on_tasks: depends_on_tasks += d.task + "," From 0174aa238c50b3ffcf70b98cafd6835988f34bae Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 10 Jan 2022 13:38:54 +0530 Subject: [PATCH 42/94] fix: Exclude unpublished items while fetching items from other item groups --- erpnext/e_commerce/product_data_engine/query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index ddd9987bb92..b927b0146e1 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -197,7 +197,10 @@ class ProductQuery: website_item_groups = frappe.db.get_all( "Website Item", fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], - filters=[["Website Item Group", "item_group", "=", item_group]] + filters=[ + ["Website Item Group", "item_group", "=", item_group], + ["published", "=", 1] + ] ) return website_item_groups From 23f4b51e3e260aadafc8fc4611f1bf1015782271 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 10 Jan 2022 18:04:03 +0530 Subject: [PATCH 43/94] fix: Behaviour if Item Variants Cache generation - Revert to old behaviour where variant doesnt need to be published - Delete unused functions --- .../doctype/website_item/website_item.py | 108 +----------------- .../variant_selector/item_variants_cache.py | 33 ++++-- 2 files changed, 25 insertions(+), 116 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 2e60dfd9459..5aedd686149 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -203,16 +203,15 @@ class WebsiteItem(WebsiteGenerator): context.body_class = "product-page" context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs - self.attributes = frappe.get_all("Item Variant Attribute", + self.attributes = frappe.get_all( + "Item Variant Attribute", fields=["attribute", "attribute_value"], - filters={"parent": self.item_code}) + filters={"parent": self.item_code} + ) if self.slideshow: context.update(get_slideshow(self)) - self.set_variant_context(context) - self.set_attribute_context(context) - self.set_disabled_attributes(context) self.set_metatags(context) self.set_shopping_cart_data(context) @@ -237,61 +236,6 @@ class WebsiteItem(WebsiteGenerator): return context - def set_variant_context(self, context): - if not self.has_variants: - return - - context.no_cache = True - variant = frappe.form_dict.variant - - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all( - "Item", - filters={ - "variant_of": self.item_code, - "published_in_website": 1 - }, - order_by="name asc") - - # the case when the item is opened for the first time from its list - if not variant and context.variants: - variant = context.variants[0] - - if variant: - context.variant = frappe.get_doc("Item", variant) - fields = ("website_image", "website_image_alt", "web_long_description", "description", - "website_specifications") - - for fieldname in fields: - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] - - context[fieldname] = value - - if self.slideshow and context.variant and context.variant.slideshow: - context.update(get_slideshow(context.variant)) - - - def set_attribute_context(self, context): - if not self.has_variants: - return - - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} - - # load attributes - self.set_selected_attributes(context.variants, context, attribute_values_available) - - # filter attributes, order based on attribute table - item = frappe.get_cached_doc("Item", self.item_code) - self.set_attribute_values(item.attributes, context, attribute_values_available) - - context.variant_info = json.dumps(context.variants) - def set_selected_attributes(self, variants, context, attribute_values_available): for variant in variants: variant.attributes = frappe.get_all( @@ -328,50 +272,6 @@ class WebsiteItem(WebsiteGenerator): if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): values.append(attr_value.attribute_value) - def set_disabled_attributes(self, context): - """Disable selection options of attribute combinations that do not result in a variant""" - - if not self.attributes or not self.has_variants: - return - - context.disabled_attributes = {} - attributes = [attr.attribute for attr in self.attributes] - - def find_variant(combination): - for variant in context.variants: - if len(variant.attributes) < len(attributes): - continue - - if "combination" not in variant: - ref_combination = [] - - for attr in variant.attributes: - idx = attributes.index(attr.attribute) - ref_combination.insert(idx, attr.attribute_value) - - variant["combination"] = ref_combination - - if not (set(combination) - set(variant["combination"])): - # check if the combination is a subset of a variant combination - # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] - return True - - for i, attr in enumerate(self.attributes): - if i == 0: - continue - - combination_source = [] - - # loop through previous attributes - for prev_attr in self.attributes[:i]: - combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) - - combination_source.append(context.attribute_values[attr.attribute]) - - for combination in itertools.product(*combination_source): - if not find_variant(combination): - context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) - def set_metatags(self, context): context.metatags = frappe._dict({}) diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 39eb9155d5e..bb6b3ef37fe 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -44,7 +44,7 @@ class ItemVariantsCacheManager: val = frappe.cache().get_value('ordered_attribute_values_map') if val: return val - all_attribute_values = frappe.db.get_all('Item Attribute Value', + all_attribute_values = frappe.get_all('Item Attribute Value', ['attribute_value', 'idx', 'parent'], order_by='idx asc') ordered_attribute_values_map = frappe._dict({}) @@ -57,25 +57,34 @@ class ItemVariantsCacheManager: def build_cache(self): parent_item_code = self.item_code - attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute', - {'parent': parent_item_code}, ['attribute'], order_by='idx asc') + attributes = [ + a.attribute for a in frappe.get_all( + 'Item Variant Attribute', + {'parent': parent_item_code}, + ['attribute'], + order_by='idx asc' + ) ] - item_variants_data = frappe.db.get_all('Item Variant Attribute', - {'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'], + # join with Website Item + item_variants_data = frappe.get_all( + 'Item Variant Attribute', + {'variant_of': parent_item_code}, + ['parent', 'attribute', 'attribute_value'], order_by='name', as_list=1 ) - unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])]) + disabled_items = set( + [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] + ) - attribute_value_item_map = frappe._dict({}) - item_attribute_value_map = frappe._dict({}) + attribute_value_item_map = frappe._dict() + item_attribute_value_map = frappe._dict() - # dont consider variants that are unpublished - # (either have no Website Item or are unpublished in Website Item) - item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items] - item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})] + # dont consider variants that are disabled + # pull all other variants + item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] for row in item_variants_data: item_code, attribute, attribute_value = row From 055d620ef3a53f0c2a6f9bab370afe5f5785dd0f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 19:48:28 +0530 Subject: [PATCH 44/94] fix: pos invoices consolidation case with permlevel (#29220) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 545abf77e6b..5062c1c807a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -651,7 +651,7 @@ "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, - "permlevel": 1, + "permlevel": 0, "print_hide": 1 }, { @@ -2038,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-21 20:19:38.667508", + "modified": "2021-12-23 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From fbe0442e89b749861fa1de4bcf529be991bd1b25 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 4 Jan 2022 16:36:08 +0530 Subject: [PATCH 45/94] fix(patch): serial no whitespace trimming old data can contain trailing/leading whitespace which doesn't work well with code to find last SLE for serial no. (cherry picked from commit 0faa116f9799f6d921ce8868a8f8eac1756ae008) --- erpnext/patches.txt | 1 + .../v13_0/trim_whitespace_from_serial_nos.py | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 35ad56a5193..2b01c0b8507 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -305,6 +305,7 @@ erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.remove_bad_selling_defaults +erpnext.patches.v13_0.trim_whitespace_from_serial_nos erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py new file mode 100644 index 00000000000..4f112550c51 --- /dev/null +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -0,0 +1,61 @@ +import frappe + +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + +def execute(): + broken_sles = frappe.db.sql(""" + select name, serial_no + from `tabStock Ledger Entry` + where + is_cancelled = 0 + and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s) + """, + ( + " %", # leading whitespace + "% ", # trailing whitespace + "%\n %", # leading whitespace on newline + "% \n%", # trailing whitespace on newline + ), + as_dict=True, + ) + + frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles) + + if not broken_sles: + return + + broken_serial_nos = set() + + for sle in broken_sles: + serial_no_list = get_serial_nos(sle.serial_no) + correct_sr_no = "\n".join(serial_no_list) + + if correct_sr_no == sle.serial_no: + continue + + frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False) + broken_serial_nos.update(serial_no_list) + + if not broken_serial_nos: + return + + broken_sr_no_records = [sr[0] for sr in frappe.db.sql(""" + select name + from `tabSerial No` + where status='Active' + and coalesce(purchase_document_type, '') = '' + and name in %s """, (list(broken_serial_nos),) + )] + + frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records) + + patch_savepoint = "serial_no_patch" + for serial_no in broken_sr_no_records: + try: + frappe.db.savepoint(patch_savepoint) + sn = frappe.get_doc("Serial No", serial_no) + sn.update_serial_no_reference() + sn.db_update() + except Exception: + frappe.db.rollback(save_point=patch_savepoint) From d0c7d4fc9841a15aa930745e38b8c9c17e0d46da Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 10 Jan 2022 16:09:43 +0530 Subject: [PATCH 46/94] refactor: convert query to ORM (cherry picked from commit cbaa8fdade4aad306887b23cef9bfeaa17ff07c0) --- .../v13_0/trim_whitespace_from_serial_nos.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index 4f112550c51..8a9633d8964 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -27,6 +27,7 @@ def execute(): broken_serial_nos = set() + # patch SLEs for sle in broken_sles: serial_no_list = get_serial_nos(sle.serial_no) correct_sr_no = "\n".join(serial_no_list) @@ -40,13 +41,16 @@ def execute(): if not broken_serial_nos: return - broken_sr_no_records = [sr[0] for sr in frappe.db.sql(""" - select name - from `tabSerial No` - where status='Active' - and coalesce(purchase_document_type, '') = '' - and name in %s """, (list(broken_serial_nos),) - )] + # Patch serial No documents if they don't have purchase info + # Purchase info is used for fetching incoming rate + broken_sr_no_records = frappe.get_list("Serial No", + filters={ + "status":"Active", + "name": ("in", broken_serial_nos), + "purchase_document_type": ("is", "not set") + }, + pluck="name", + ) frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records) From 4343eddd46669047a845a2b8a31c5490063ddfc7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 13:24:21 +0530 Subject: [PATCH 47/94] fix(gl-report): group by cost center only if include_dimensions is checked (#28883) (#29231) --- .../report/general_ledger/general_ledger.js | 2 +- .../report/general_ledger/general_ledger.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index b2968761c63..010284c2ea5 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = { "fieldname": "include_dimensions", "label": __("Consider Accounting Dimensions"), "fieldtype": "Check", - "default": 0 + "default": 1 }, { "fieldname": "show_opening_entries", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 9aad52137b4..452a60d3055 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): elif group_by_voucher_consolidated: keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] - for dim in accounting_dimensions: - keylist.append(gle.get(dim)) - keylist.append(gle.get("cost_center")) + if filters.get("include_dimensions"): + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) @@ -595,14 +597,14 @@ def get_columns(filters): "fieldname": dim.fieldname, "width": 100 }) - - columns.extend([ - { + columns.append({ "label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100 - }, + }) + + columns.extend([ { "label": _("Against Voucher Type"), "fieldname": "against_voucher_type", From 8bb72dece451e3a6fae3262f6e847e2ad6be92aa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 14:26:39 +0530 Subject: [PATCH 48/94] fix(pos): cannot ignore pricing rule for one particular invoice (#29232) --- .../doctype/pos_invoice/pos_invoice.py | 1 - .../doctype/pos_invoice/test_pos_invoice.py | 31 +++++++++++++++++++ .../doctype/pricing_rule/test_pricing_rule.py | 4 ++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index d5961223358..de31b5c4136 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -354,7 +354,6 @@ class POSInvoice(SalesInvoice): if not for_validate and not self.customer: self.customer = profile.customer - self.ignore_pricing_rule = profile.ignore_pricing_rule self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount self.set_warehouse = profile.get('warehouse') or self.set_warehouse diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 7d31e0aa195..56479a0b77d 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -556,6 +556,37 @@ class TestPOSInvoice(unittest.TestCase): batch.cancel() batch.delete() + def test_ignore_pricing_rule(self): + from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule + + item_price = frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Item', + 'price_list': '_Test Price List', + 'price_list_rate': '450', + }) + item_price.insert() + pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) + pr.save() + pos_inv = create_pos_invoice(qty=1, do_not_submit=1) + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].discount_percentage, 10) + # rate shouldn't change + self.assertEquals(pos_inv.items[0].rate, 405) + + pos_inv.ignore_pricing_rule = 1 + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.ignore_pricing_rule, 1) + # rate should change since pricing rules are ignored + self.assertEquals(pos_inv.items[0].rate, 300) + + item_price.delete() + pos_inv.delete() + pr.delete() + + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 74e188471de..e9a018ec46c 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -652,7 +652,7 @@ def make_pricing_rule(**args): "rate": args.rate or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "condition": args.condition or '', - "priority": 1, + "priority": args.priority or 1, "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 }) @@ -678,6 +678,8 @@ def make_pricing_rule(**args): if args.get(applicable_for): doc.db_set(applicable_for, args.get(applicable_for)) + return doc + def setup_pricing_rule_data(): if not frappe.db.exists('Campaign', '_Test Campaign'): frappe.get_doc({ From 819346f69fa789f268d79708aa49f54efd821361 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 09:02:57 +0000 Subject: [PATCH 49/94] fix: "update cost" should ignore overridden routing times #29154 (#29235) fix: "update cost" should ignore overridden routing times (cherry picked from commit 754596dfc139a9890b1e446a4d2b2abfeab68449) Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom/bom.py | 10 ---------- erpnext/manufacturing/doctype/routing/test_routing.py | 5 +++-- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0ac64c2cfca..f75038eab34 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -531,16 +531,6 @@ class BOM(WebsiteGenerator): row.hour_rate = (hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate) - if self.routing: - time_in_mins = flt(frappe.db.get_value("BOM Operation", { - "workstation": row.workstation, - "operation": row.operation, - "parent": self.routing - }, ["time_in_mins"])) - - if time_in_mins: - row.time_in_mins = time_in_mins - if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index e90b0a7d6d2..8bd60ea4aca 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase): wo_doc.delete() def test_update_bom_operation_time(self): + """Update cost shouldn't update routing times.""" operations = [ { "operation": "Test Operation A", @@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase): routing_doc.save() bom_doc.update_cost() bom_doc.reload() - self.assertEqual(bom_doc.operations[0].time_in_mins, 90) - self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + self.assertEqual(bom_doc.operations[0].time_in_mins, 30) + self.assertEqual(bom_doc.operations[1].time_in_mins, 20) def setup_operations(rows): From de9429b58559355ccd370041bdaaa70d23dbecb5 Mon Sep 17 00:00:00 2001 From: ruthra Date: Fri, 7 Jan 2022 16:30:56 +0530 Subject: [PATCH 50/94] fix: deferred report division by zero exception (cherry picked from commit fe4d7f86ee7384e794c54b32e5b5f4415654f02a) --- .../deferred_revenue_and_expense.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index a4842c1844f..3a51db8a97f 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -121,20 +121,21 @@ class Deferred_Item(object): """ simulate future posting by creating dummy gl entries. starts from the last posting date. """ - if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: - self.estimate_for_period_list = get_period_list( - self.filters.from_fiscal_year, - self.filters.to_fiscal_year, - add_days(self.last_entry_date, 1), - self.period_list[-1].to_date, - "Date Range", - "Monthly", - company=self.filters.company, - ) - for period in self.estimate_for_period_list: - amount = self.calculate_amount(period.from_date, period.to_date) - gle = self.make_dummy_gle(period.key, period.to_date, amount) - self.gle_entries.append(gle) + if self.service_start_date != self.service_end_date: + if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: + self.estimate_for_period_list = get_period_list( + self.filters.from_fiscal_year, + self.filters.to_fiscal_year, + add_days(self.last_entry_date, 1), + self.period_list[-1].to_date, + "Date Range", + "Monthly", + company=self.filters.company, + ) + for period in self.estimate_for_period_list: + amount = self.calculate_amount(period.from_date, period.to_date) + gle = self.make_dummy_gle(period.key, period.to_date, amount) + self.gle_entries.append(gle) def calculate_item_revenue_expense_for_period(self): """ From 577de39ca6c8fb1709a7fd14354840ade8d5a42a Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 11 Jan 2022 18:19:29 +0530 Subject: [PATCH 51/94] test: Variant Selector - Attribute Value fetching --- .../doctype/website_item/website_item.py | 1 - .../variant_selector/test_variant_selector.py | 87 +++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 5aedd686149..181aaa1292e 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -1,7 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import itertools import json import frappe diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index c70fee5fff5..d9f56216333 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,11 +1,88 @@ -# import frappe +import frappe import unittest -# from erpnext.e_commerce.product_data_engine.query import ProductQuery -# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item +from erpnext.controllers.item_variant import create_variant +from erpnext.e_commerce.doctype.website_item.website_item import make_website_item +from erpnext.stock.doctype.item.test_item import make_item test_dependencies = ["Item"] class TestVariantSelector(unittest.TestCase): - # TODO: Variant Selector Tests - pass \ No newline at end of file + + def setUp(self) -> None: + self.template_item = make_item("Test-Tshirt-Temp", { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [ + { + "attribute": "Test Size" + }, + { + "attribute": "Test Colour" + } + ] + }) + + # create L-R, L-G, M-R, M-G and S-R + for size in ("Large", "Medium",): + for colour in ("Red", "Green",): + variant = create_variant("Test-Tshirt-Temp", { + "Test Size": size, + "Test Colour": colour + }) + variant.save() + + variant = create_variant("Test-Tshirt-Temp", { + "Test Size": "Small", + "Test Colour": "Red" + }) + variant.save() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_item_attributes(self): + """ + Test if the right attributes are fetched in the popup. + (Attributes must only come from active items) + + Attribute selection must not be linked to Website Items. + """ + from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values + + make_website_item(self.template_item) # publish template not variants + + attr_data = get_attributes_and_values("Test-Tshirt-Temp") + + self.assertEqual(attr_data[0]["attribute"], "Test Size") + self.assertEqual(attr_data[1]["attribute"], "Test Colour") + self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large'] + self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green'] + + # disable small red tshirt, now there are no small tshirts. + # but there are some red tshirts + small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R") + small_variant.disabled = 1 + small_variant.save() # trigger cache rebuild + + attr_data = get_attributes_and_values("Test-Tshirt-Temp") + + # Only L and M attribute values must be fetched since S is disabled + self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large'] + + # def test_next_item_variant_values(self): + # """ + # Test if on selecting an attribute value, the next possible values + # are filtered accordingly. + # Values that dont apply should not be fetched. + # E.g. + # There is a ** Small-Red ** Tshirt. No other colour in this size. + # On selecting ** Small **, only ** Red ** should be selectable next. + # """ + # from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values + + # next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={ + # "Colour": "Red" + # }) + # print(next_values) \ No newline at end of file From c1fbd2308cc5a67ae182a68a4ab3475068e668b2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 11 Jan 2022 17:06:31 +0530 Subject: [PATCH 52/94] fix: UOM autocomplete broken All new recent sites seem to have all UOMs as disabled by default. The desired behaviour is exact opposite of this. (cherry picked from commit 33aad4b950045d90eb4505274fd8d504776608a8) --- erpnext/controllers/tests/test_queries.py | 5 +++++ erpnext/setup/setup_wizard/operations/install_fixtures.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 05541d16887..908d78c15bf 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -1,6 +1,8 @@ import unittest from functools import partial +import frappe + from erpnext.controllers import queries @@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase): wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]]) self.assertGreaterEqual(len(wh), 1) + + def test_default_uoms(self): + self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index dbf991ceff3..d76d97663cd 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -350,7 +350,8 @@ def add_uom_data(): "doctype": "UOM", "uom_name": _(d.get("uom_name")), "name": _(d.get("uom_name")), - "must_be_whole_number": d.get("must_be_whole_number") + "must_be_whole_number": d.get("must_be_whole_number"), + "enabled": 1, }).db_insert() # bootstrap uom conversion factors From d9f31770c659b2da8a69f9a518ae93a2bf68cb8f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 11 Jan 2022 17:22:40 +0530 Subject: [PATCH 53/94] fix(patch): enable all uoms on recently created sites (cherry picked from commit f8119563ca51f46917cc521429d61a374b6cbc8a) --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/enable_uoms.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 erpnext/patches/v13_0/enable_uoms.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2b01c0b8507..18319f70c47 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -331,6 +331,7 @@ erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.update_job_card_status +erpnext.patches.v13_0.enable_uoms erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py new file mode 100644 index 00000000000..4d3f6376303 --- /dev/null +++ b/erpnext/patches/v13_0/enable_uoms.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + frappe.reload_doc('setup', 'doctype', 'uom') + + uom = frappe.qb.DocType("UOM") + + (frappe.qb + .update(uom) + .set(uom.enabled, 1) + .where(uom.creation >= "2021-10-18") # date when this field was released + ).run() From 746bb6277f2f6c240df226012ceb90a6baf5b339 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 11 Jan 2022 20:15:24 +0530 Subject: [PATCH 54/94] test: Variant Selector - Fetch next valid Attribute values fetching after selection of one --- .../variant_selector/test_variant_selector.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index d9f56216333..608ff230a67 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,6 +1,7 @@ -import frappe import unittest +import frappe + from erpnext.controllers.item_variant import create_variant from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.stock.doctype.item.test_item import make_item @@ -71,18 +72,22 @@ class TestVariantSelector(unittest.TestCase): # Only L and M attribute values must be fetched since S is disabled self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large'] - # def test_next_item_variant_values(self): - # """ - # Test if on selecting an attribute value, the next possible values - # are filtered accordingly. - # Values that dont apply should not be fetched. - # E.g. - # There is a ** Small-Red ** Tshirt. No other colour in this size. - # On selecting ** Small **, only ** Red ** should be selectable next. - # """ - # from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values + def test_next_item_variant_values(self): + """ + Test if on selecting an attribute value, the next possible values + are filtered accordingly. + Values that dont apply should not be fetched. + E.g. + There is a ** Small-Red ** Tshirt. No other colour in this size. + On selecting ** Small **, only ** Red ** should be selectable next. + """ + from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values - # next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={ - # "Colour": "Red" - # }) - # print(next_values) \ No newline at end of file + next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"}) + next_colours = next_values["valid_options_for_attributes"]["Test Colour"] + filtered_items = next_values["filtered_items"] + + self.assertEqual(len(next_colours), 1) + self.assertEqual(next_colours.pop(), "Red") + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R") From 00ad0dd24d47f3cac45fedae599db0d194860927 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 21:11:42 +0530 Subject: [PATCH 55/94] feat: early payment discount on sales & purchase orders (#29238) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 61fa194aefd..c8c214500c9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1709,7 +1709,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount = 0 - if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule: + eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice'] + has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule + + if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: if term.discount_type == 'Percentage': From a0bd82e99f617071ce21124c9cd3638c917a74b7 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 11 Jan 2022 22:43:39 +0530 Subject: [PATCH 56/94] fix: Server Side test - teardown correctly --- .../e_commerce/variant_selector/test_variant_selector.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 608ff230a67..31c86f1fd53 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -39,8 +39,7 @@ class TestVariantSelector(unittest.TestCase): }) variant.save() - @classmethod - def tearDownClass(cls): + def tearDown(self): frappe.db.rollback() def test_item_attributes(self): @@ -72,6 +71,10 @@ class TestVariantSelector(unittest.TestCase): # Only L and M attribute values must be fetched since S is disabled self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large'] + # teardown + small_variant.disabled = 1 + small_variant.save() + def test_next_item_variant_values(self): """ Test if on selecting an attribute value, the next possible values From d71d769c7aee84f09a9564aecd13f81333c9b839 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 6 Jan 2022 22:12:31 +0530 Subject: [PATCH 57/94] fix: incorrect scrap item qty (cherry picked from commit 8f0b2fa90ed395a0fcbefb5e099ef60fc79bf6aa) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 79 ++++++++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 39 +++++---- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index aa19b2f1003..e385c578fc9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.utils import add_months, cint, flt, now, today +from frappe.utils import add_days, add_months, cint, flt, now, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( OverProductionError, StockOverProductionError, close_work_order, + make_job_card, make_stock_entry, stop_unstop, ) @@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase): if row.is_scrap_item: self.assertEqual(row.qty, 1) + # Partial Job Card 1 with qty 10 + wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + + # Partial Job Card 2 with qty 10 + operations = [] + wo_order.load_from_db() + for row in wo_order.operations: + n_dict = row.as_dict() + n_dict['qty'] = 10 + n_dict['pending_qty'] = 10 + operations.append(n_dict) + + make_job_card(wo_order.name, operations) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + def test_close_work_order(self): items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO'] @@ -841,7 +870,51 @@ class TestWorkOrder(ERPNextTestCase): close_work_order(wo_order, "Closed") self.assertEqual(wo_order.get('status'), "Closed") +<<<<<<< HEAD def update_job_card(job_card): +======= + def test_fix_time_operations(self): + bom = frappe.get_doc({ + "doctype": "BOM", + "item": "_Test FG Item 2", + "is_active": 1, + "is_default": 1, + "quantity": 1.0, + "with_operations": 1, + "operations": [ + { + "operation": "_Test Operation 1", + "description": "_Test", + "workstation": "_Test Workstation 1", + "time_in_mins": 60, + "operating_cost": 140, + "fixed_time": 1 + } + ], + "items": [ + { + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "items", + "qty": 1.0, + "rate": 5000.0, + }, + ], + }) + bom.save() + bom.submit() + + + wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1) + wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1) + + self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins) + + +def update_job_card(job_card, jc_qty=None): + employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') +>>>>>>> 8f0b2fa90e (fix: incorrect scrap item qty) job_card_doc = frappe.get_doc('Job Card', job_card) job_card_doc.set('scrap_items', [ { @@ -854,8 +927,12 @@ def update_job_card(job_card): }, ]) + if jc_qty: + job_card_doc.for_quantity = jc_qty + job_card_doc.append('time_logs', { 'from_time': now(), + 'employee': employee, 'time_in_mins': 60, 'completed_qty': job_card_doc.for_quantity }) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b4865237100..3e9197c7e19 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -8,6 +8,7 @@ from collections import defaultdict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate from six import iteritems, itervalues, string_types @@ -1276,22 +1277,29 @@ class StockEntry(StockController): if not self.pro_doc: self.set_work_order_details() - scrap_items = frappe.db.sql(''' - SELECT - JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description - FROM - `tabJob Card` JC, `tabJob Card Scrap Item` JCSI - WHERE - JCSI.parent = JC.name AND JC.docstatus = 1 - AND JCSI.item_code IS NOT NULL AND JC.work_order = %s - GROUP BY - JCSI.item_code - ''', self.work_order, as_dict=1) - - pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty) - if pending_qty <=0: + if not self.pro_doc.operations: return [] + job_card = frappe.qb.DocType('Job Card') + job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item') + + scrap_items = ( + frappe.qb.from_(job_card) + .select( + Sum(job_card_scrap_item.stock_qty).as_('stock_qty'), + job_card_scrap_item.item_code, job_card_scrap_item.item_name, + job_card_scrap_item.description, job_card_scrap_item.stock_uom) + .join(job_card_scrap_item) + .on(job_card_scrap_item.parent == job_card.name) + .where( + (job_card_scrap_item.item_code.isnotnull()) + & (job_card.work_order == self.work_order) + & (job_card.docstatus == 1)) + .groupby(job_card_scrap_item.item_code) + ).run(as_dict=1) + + pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty) + used_scrap_items = self.get_used_scrap_items() for row in scrap_items: row.stock_qty -= flt(used_scrap_items.get(row.item_code)) @@ -1305,6 +1313,9 @@ class StockEntry(StockController): return scrap_items + def get_completed_job_card_qty(self): + return flt(min([d.completed_qty for d in self.pro_doc.operations])) + def get_used_scrap_items(self): used_scrap_items = defaultdict(float) data = frappe.get_all( From fa66d1d87dd5bb225c00a56c43c2c81820c672ff Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 12 Jan 2022 13:30:05 +0530 Subject: [PATCH 58/94] fix: conflicts --- .../manufacturing/doctype/work_order/test_work_order.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e385c578fc9..1b3fdd16fba 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -870,9 +870,6 @@ class TestWorkOrder(ERPNextTestCase): close_work_order(wo_order, "Closed") self.assertEqual(wo_order.get('status'), "Closed") -<<<<<<< HEAD -def update_job_card(job_card): -======= def test_fix_time_operations(self): bom = frappe.get_doc({ "doctype": "BOM", @@ -911,10 +908,9 @@ def update_job_card(job_card): self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins) - def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') ->>>>>>> 8f0b2fa90e (fix: incorrect scrap item qty) + job_card_doc = frappe.get_doc('Job Card', job_card) job_card_doc.set('scrap_items', [ { @@ -939,7 +935,6 @@ def update_job_card(job_card, jc_qty=None): job_card_doc.submit() - def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` From f4a031d94b7d364ce710f5281dd9278bae900cdb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:31:07 +0530 Subject: [PATCH 59/94] fix: description not fetched in sales order analytics report (#29254) --- .../report/sales_order_analysis/sales_order_analysis.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 82e5d0ce57d..c530d30c0c2 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -67,7 +67,8 @@ def get_data(conditions, filters): (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, (soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount, soi.warehouse as warehouse, - so.company, soi.name + so.company, soi.name, + soi.description as description FROM `tabSales Order` so, `tabSales Order Item` soi @@ -179,6 +180,12 @@ def get_columns(filters): "options": "Item", "width": 100 }) + columns.append({ + "label":_("Description"), + "fieldname": "description", + "fieldtype": "Small Text", + "width": 100 + }) columns.extend([ { From b4fee6d0657e255e791ad8eaa8fb6675b31a982e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 12 Jan 2022 13:31:34 +0530 Subject: [PATCH 60/94] fix: conflicts --- .../doctype/work_order/test_work_order.py | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 1b3fdd16fba..1c9cd0cbf4d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -870,44 +870,6 @@ class TestWorkOrder(ERPNextTestCase): close_work_order(wo_order, "Closed") self.assertEqual(wo_order.get('status'), "Closed") - def test_fix_time_operations(self): - bom = frappe.get_doc({ - "doctype": "BOM", - "item": "_Test FG Item 2", - "is_active": 1, - "is_default": 1, - "quantity": 1.0, - "with_operations": 1, - "operations": [ - { - "operation": "_Test Operation 1", - "description": "_Test", - "workstation": "_Test Workstation 1", - "time_in_mins": 60, - "operating_cost": 140, - "fixed_time": 1 - } - ], - "items": [ - { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "items", - "qty": 1.0, - "rate": 5000.0, - }, - ], - }) - bom.save() - bom.submit() - - - wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1) - wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1) - - self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins) - def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') From 366f663c5a0e2a6d70028512c4861f7b32954dc2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:45:37 +0530 Subject: [PATCH 61/94] fix(asset): check if fixed asset account is set against company (#29256) --- erpnext/assets/doctype/asset/asset.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 90257c784ee..a57a3e281d3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -583,7 +583,17 @@ class Asset(AccountsController): return purchase_document def get_fixed_asset_account(self): - return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + if not fixed_asset_account: + frappe.throw( + _("Set {0} in asset category {1} for company {2}").format( + frappe.bold("Fixed Asset Account"), + frappe.bold(self.asset_category), + frappe.bold(self.company), + ), + title=_("Account not Found"), + ) + return fixed_asset_account def get_cwip_account(self, cwip_enabled=False): cwip_account = None From d01e794558060325471ba10f8a29617fb3103472 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 12 Jan 2022 16:44:33 +0530 Subject: [PATCH 62/94] Merge pull request #29257 from marination/fix-reset-wh-defaults fix: Avoid resetting Default wh fields for Manufacture Entry (cherry picked from commit efcfb825d7a72a403f06f62e322cedd7fbbffd29) --- .../tests/test_transaction_base.py | 79 ++++++++++++++++--- .../stock/doctype/stock_entry/stock_entry.py | 8 +- erpnext/utilities/transaction_base.py | 2 - 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py index 13aa697610e..f4d3f97ef0d 100644 --- a/erpnext/controllers/tests/test_transaction_base.py +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -4,19 +4,72 @@ import frappe class TestUtils(unittest.TestCase): - def test_reset_default_field_value(self): - doc = frappe.get_doc({ - "doctype": "Purchase Receipt", - "set_warehouse": "Warehouse 1", - }) + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) - # Same values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, "Warehouse 1") + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") - # Mixed values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, None) + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + def test_reset_default_field_value_in_mfg_stock_entry(self): + # manufacture stock entry with rows having blank source/target wh + se = frappe.get_doc( + doctype="Stock Entry", + purpose="Manufacture", + stock_entry_type="Manufacture", + company="_Test Company", + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + items=[ + frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1) + ] + ) + se.save() + + # default fields must be untouched + self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC") + self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC") + + se.delete() + + def test_reset_default_field_value_in_transfer_stock_entry(self): + doc = frappe.get_doc({ + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "from_warehouse": "Warehouse 1", + "to_warehouse": "Warehouse 2", + }) + + # Same values + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, "Warehouse 1") + self.assertEqual(doc.to_warehouse, "Warehouse 2") + + # Mixed values in source wh + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, None) + self.assertEqual(doc.to_warehouse, "Warehouse 2") \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3e9197c7e19..92f67328043 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -111,8 +111,12 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() - self.reset_default_field_value("from_warehouse", "items", "s_warehouse") - self.reset_default_field_value("to_warehouse", "items", "t_warehouse") + + if not self.get("purpose") == "Manufacture": + # ignore scrap item wh difference and empty source/target wh + # in Manufacture Entry + self.reset_default_field_value("from_warehouse", "items", "s_warehouse") + self.reset_default_field_value("to_warehouse", "items", "t_warehouse") def on_submit(self): self.update_stock_ledger() diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 76c183447ae..1c11c6aba6e 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -182,8 +182,6 @@ class TransactionBase(StatusUpdater): if len(child_table_values) > 1: self.set(default_field, None) - else: - self.set(default_field, list(child_table_values)[0]) def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT From dcea4f4c969a638c9b0d6e5e5ff657ac8921edaf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 12 Jan 2022 19:54:59 +0530 Subject: [PATCH 63/94] fix: autoname generated for Job Applicant is too long (backport #29260) (#29263) * fix: autoname generated for Job Applicant is too long (#29260) * fix: autoname generated for Job Applicant is too long - autoname based on email and append number if exists instead of concatenating name, email, title - add more search fields for context during selection * test: Job applicant naming and fix related tests (cherry picked from commit 5cda4ea39f85456e6d68df0c6a6498f98a052734) # Conflicts: # erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py * fix: conflicts Co-authored-by: Rucha Mahabal --- .../test_employee_onboarding.py | 11 ++++++---- .../doctype/job_applicant/job_applicant.json | 6 ++++-- .../hr/doctype/job_applicant/job_applicant.py | 10 +++++---- .../job_applicant/test_job_applicant.py | 21 ++++++++++++++++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index df6e9bde006..daa068e6e03 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase): def test_employee_onboarding_incomplete_task(self): if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) - _set_up() + frappe.db.sql("delete from `tabEmployee Onboarding`") + project = "Employee Onboarding : test@researcher.com" + frappe.db.sql("delete from tabProject where name=%s", project) + frappe.db.sql("delete from tabTask where project=%s", project) applicant = get_job_applicant() job_offer = create_job_offer(job_applicant=applicant.name) @@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase): onboarding.submit() project_name = frappe.db.get_value("Project", onboarding.project, "project_name") - self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') + self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com') # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) @@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase): self.assertEqual(employee.employee_name, 'Test Researcher') def get_job_applicant(): - if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'): - return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com') + if frappe.db.exists('Job Applicant', 'test@researcher.com'): + return frappe.get_doc('Job Applicant', 'test@researcher.com') applicant = frappe.new_doc('Job Applicant') applicant.applicant_name = 'Test Researcher' applicant.email_id = 'test@researcher.com' diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index 200f675221b..66b609cf990 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.json +++ b/erpnext/hr/doctype/job_applicant/job_applicant.json @@ -192,10 +192,11 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-29 23:06:10.904260", + "modified": "2022-01-12 16:28:53.196881", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -210,10 +211,11 @@ "write": 1 } ], - "search_fields": "applicant_name", + "search_fields": "applicant_name, email_id, job_title, phone_number", "sender_field": "email_id", "sort_field": "modified", "sort_order": "ASC", + "states": [], "subject_field": "notes", "title_field": "applicant_name" } \ No newline at end of file diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index f0b470b35e8..54ccfca38f7 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.model.naming import append_number_if_name_exists from frappe.utils import validate_email_address from erpnext.hr.doctype.interview.interview import get_interviewers @@ -21,10 +22,11 @@ class JobApplicant(Document): self.get("__onload").job_offer = job_offer[0].name def autoname(self): - keys = filter(None, (self.applicant_name, self.email_id, self.job_title)) - if not keys: - frappe.throw(_("Name or Email is mandatory"), frappe.NameError) - self.name = " - ".join(keys) + self.name = self.email_id + + # applicant can apply more than once for a different job title or reapply + if frappe.db.exists("Job Applicant", self.name): + self.name = append_number_if_name_exists("Job Applicant", self.name) def validate(self): if self.email_id: diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index 36dcf6b0740..bf1622028d8 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation class TestJobApplicant(unittest.TestCase): - pass + def test_job_applicant_naming(self): + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com') + + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1') + + def tearDown(self): + frappe.db.rollback() + def create_job_applicant(**args): args = frappe._dict(args) From 81f1b7dfeb68c5a70f4aa9bc0e24f7aedcd79669 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 12 Jan 2022 23:59:52 -0500 Subject: [PATCH 64/94] fix(Payroll): Cannot submit salary slips from amended payroll entry. (#29228) * fix: salary slip transaction state after payroll entry cancel * fix: use db_set in on_cancel method Co-authored-by: Rucha Mahabal Co-authored-by: Rucha Mahabal --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e3ddaf93988..99dfc231c74 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -60,6 +60,8 @@ class PayrollEntry(Document): def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` where payroll_entry=%s """, (self.name))) + self.db_set("salary_slips_created", 0) + self.db_set("salary_slips_submitted", 0) def get_emp_list(self): """ From 21c194495c8089c10de34860fd4a7a7ed9f111cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 14:28:56 +0530 Subject: [PATCH 65/94] fix: filter for leave period in Bulk Leave Policy Assignment (backport #29272) (#29273) * fix: filter for leave period in Bulk Leave Policy Assignment * fix: set title for Leave Policy Assignment (cherry picked from commit 3a18e62a9b9cfa0c84add308f60933d83e74e6bb) Co-authored-by: Rucha Mahabal --- .../hr/doctype/leave_period/leave_period.json | 230 ++---------------- .../leave_policy_assignment.json | 5 +- .../leave_policy_assignment_list.js | 11 +- 3 files changed, 36 insertions(+), 210 deletions(-) diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json index 9e895c34fb2..84ce1147e9a 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.json +++ b/erpnext/hr/doctype/leave_period/leave_period.json @@ -1,294 +1,108 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "HR-LPR-.YYYY.-.#####", - "beta": 0, "creation": "2018-04-13 15:20:52.864288", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "from_date", + "to_date", + "is_active", + "column_break_3", + "company", + "optional_holiday_list" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_date", "fieldtype": "Date", - "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": "From Date", - "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": 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": "to_date", "fieldtype": "Date", - "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": "To Date", - "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": 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, + "default": "0", "fieldname": "is_active", "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": "Is Active", - "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": "Is Active" }, { - "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, "fieldname": "company", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "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": "optional_holiday_list", "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": "Holiday List for Optional Leave", - "length": 0, - "no_copy": 0, - "options": "Holiday List", - "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": "Holiday List" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-30 16:15:43.305502", + "links": [], + "modified": "2022-01-13 13:28:12.951025", "modified_by": "Administrator", "module": "HR", "name": "Leave Period", - "name_case": "", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "search_fields": "from_date, to_date, company", "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index 3373350e733..27f0540b247 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -113,10 +113,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-01 17:54:01.014509", + "modified": "2022-01-13 13:37:11.218882", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -164,5 +165,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js index 8b954c46a10..6b75817cba9 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = { if (cur_dialog.fields_dict.leave_period.value) { me.set_effective_date(); } - } + }, + get_query() { + let filters = {"is_active": 1}; + if (cur_dialog.fields_dict.company.value) + filters["company"] = cur_dialog.fields_dict.company.value; + + return { + filters: filters + }; + }, }, { fieldtype: "Column Break" From a868fd7df921ec82dea13f141802b6d3049f0096 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 13 Jan 2022 12:15:40 +0530 Subject: [PATCH 66/94] fix: Allow multiple fg in repack entry (cherry picked from commit eac7b5d5aaed83217b0a2843d13e75cb94dbb9a0) --- .../stock/doctype/stock_entry/stock_entry.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 92f67328043..a369e06411d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -739,9 +739,9 @@ class StockEntry(StockController): def validate_finished_goods(self): """ - 1. Check if FG exists - 2. Check if Multiple FG Items are present - 3. Check FG Item and Qty against WO if present + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] @@ -754,8 +754,9 @@ class StockEntry(StockController): for d in self.get('items'): if d.is_finished_item: if not self.work_order: + # Independent MFG Entry/ Repack Entry, no WO to match against finished_items.append(d.item_code) - continue # Independent Manufacture Entry, no WO to match against + continue if d.item_code != production_item: frappe.throw(_("Finished Item {0} does not match with Work Order {1}") @@ -768,19 +769,17 @@ class StockEntry(StockController): finished_items.append(d.item_code) - if len(set(finished_items)) > 1: + if not finished_items: frappe.throw( - msg=_("Multiple items cannot be marked as finished item"), - title=_("Note"), - exc=FinishedGoodError + msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), + title=_("Missing Finished Good"), exc=FinishedGoodError ) if self.purpose == "Manufacture": - if not finished_items: + if len(set(finished_items)) > 1: frappe.throw( - msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good"), - exc=FinishedGoodError + msg=_("Multiple items cannot be marked as finished item"), + title=_("Note"), exc=FinishedGoodError ) allowance_percentage = flt( From 61681e73ef56a9b9140156c8995a529ca654b17d Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 13 Jan 2022 14:36:21 +0530 Subject: [PATCH 67/94] test: Basic test one item repacked into two - Also run fg validation and fg marking after checking purpose, avoid unnecessary calls (cherry picked from commit 3922a39591ad79a44438f3edf28841348a64d74c) --- .../stock/doctype/stock_entry/stock_entry.py | 38 ++++++++++--------- .../doctype/stock_entry/test_stock_entry.py | 35 ++++++++++++++++- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a369e06411d..7472c95ecf5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -87,8 +87,11 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() - self.mark_finished_and_scrap_items() - self.validate_finished_goods() + + if self.purpose in ("Manufacture", "Repack"): + self.mark_finished_and_scrap_items() + self.validate_finished_goods() + self.validate_with_material_request() self.validate_batch() self.validate_inspection() @@ -707,26 +710,25 @@ class StockEntry(StockController): validate_bom_no(item_code, d.bom_no) def mark_finished_and_scrap_items(self): - if self.purpose in ("Repack", "Manufacture"): - if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): - return + if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + return - finished_item = self.get_finished_item() + finished_item = self.get_finished_item() - if not finished_item and self.purpose == "Manufacture": - # In case of independent Manufacture entry, don't auto set - # user must decide and set - return + if not finished_item and self.purpose == "Manufacture": + # In case of independent Manufacture entry, don't auto set + # user must decide and set + return - for d in self.items: - if d.t_warehouse and not d.s_warehouse: - if self.purpose=="Repack" or d.item_code == finished_item: - d.is_finished_item = 1 - else: - d.is_scrap_item = 1 + for d in self.items: + if d.t_warehouse and not d.s_warehouse: + if self.purpose=="Repack" or d.item_code == finished_item: + d.is_finished_item = 1 else: - d.is_finished_item = 0 - d.is_scrap_item = 0 + d.is_scrap_item = 1 + else: + d.is_finished_item = 0 + d.is_scrap_item = 0 def get_finished_item(self): finished_item = None diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1e0335da73d..9a7dc71eac4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -227,9 +227,40 @@ class TestStockEntry(ERPNextTestCase): mtn.cancel() - def test_repack_no_change_in_valuation(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') + def test_repack_multiple_fg(self): + "Test `is_finsihed_item` for one item repacked into two items." + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) + repack = frappe.copy_doc(test_records[3]) + repack.posting_date = nowdate() + repack.posting_time = nowtime() + + repack.items[0].qty = 100.0 + repack.items[0].transfer_qty = 100.0 + repack.items[1].qty = 50.0 + repack.items[1].basic_rate = 200 + + repack.append("items", { + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - _TC", + "basic_rate": 150, + "item_code": "_Test Item 2", + "parentfield": "items", + "qty": 50.0, + "stock_uom": "_Test UOM", + "t_warehouse": "_Test Warehouse - _TC", + "transfer_qty": 50.0, + "uom": "_Test UOM" + }) + repack.set_stock_entry_type() + repack.insert() + + self.assertEqual(repack.items[1].is_finished_item, 1) + self.assertEqual(repack.items[2].is_finished_item, 1) + + def test_repack_no_change_in_valuation(self): make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100) From 80b037ed1313d826452148340642eadf3a5b9f19 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 13 Jan 2022 15:02:59 +0530 Subject: [PATCH 68/94] test: Check for FinishedGoodError if 0 FG in repack entry (cherry picked from commit c49dff385a4798e329f0a8390563afee945c7656) --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 9a7dc71eac4..af7acc36353 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -228,7 +228,7 @@ class TestStockEntry(ERPNextTestCase): mtn.cancel() def test_repack_multiple_fg(self): - "Test `is_finsihed_item` for one item repacked into two items." + "Test `is_finished_item` for one item repacked into two items." make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) repack = frappe.copy_doc(test_records[3]) @@ -238,7 +238,6 @@ class TestStockEntry(ERPNextTestCase): repack.items[0].qty = 100.0 repack.items[0].transfer_qty = 100.0 repack.items[1].qty = 50.0 - repack.items[1].basic_rate = 200 repack.append("items", { "conversion_factor": 1.0, @@ -260,6 +259,14 @@ class TestStockEntry(ERPNextTestCase): self.assertEqual(repack.items[1].is_finished_item, 1) self.assertEqual(repack.items[2].is_finished_item, 1) + repack.items[1].is_finished_item = 0 + repack.items[2].is_finished_item = 0 + + # must raise error if 0 fg in repack entry + self.assertRaises(FinishedGoodError, repack.validate_finished_goods) + + repack.delete() # teardown + def test_repack_no_change_in_valuation(self): make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", From e28b046cf9f69553354f5f4bdacb6232d16e5466 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 13 Jan 2022 12:51:34 +0530 Subject: [PATCH 69/94] fix: accounts are coming from different company in the dropdown (cherry picked from commit dabe5981bbb54a5b2dff704481c4eb3e1676c3fb) --- erpnext/setup/doctype/company/company.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 45e8dccc319..dd185fc6636 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) { ["default_payroll_payable_account", {"root_type": "Liability"}], ["round_off_account", {"root_type": "Expense"}], ["write_off_account", {"root_type": "Expense"}], + ["default_deferred_expense_account", {}], + ["default_deferred_revenue_account", {}], + ["default_expense_claim_payable_account", {}], ["default_discount_account", {}], ["discount_allowed_account", {"root_type": "Expense"}], ["discount_received_account", {"root_type": "Income"}], From 94a30674dff0e45540e1641234411dfdc8ffc780 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 14 Jan 2022 15:29:46 +0530 Subject: [PATCH 70/94] fix: threshold fields shows incorrect currency (#29289) --- .../doctype/tax_withholding_rate/tax_withholding_rate.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json index d2c505c6300..e032bb307b0 100644 --- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json +++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json @@ -28,14 +28,14 @@ { "columns": 2, "fieldname": "single_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Single Transaction Threshold" }, { "columns": 3, "fieldname": "cumulative_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Cumulative Transaction Threshold" }, @@ -59,7 +59,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-31 11:42:12.213977", + "modified": "2022-01-13 12:04:42.904263", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Rate", @@ -68,5 +68,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 835448c9e5d60752c232fe53057396dc3e16165c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 10 Jan 2022 12:50:34 +0530 Subject: [PATCH 71/94] fix(India): NIL Rated, Exempted and non gst invoices in GSTR-1 report (cherry picked from commit 7d85755595149fb27638126f5d05eb79734fc548) --- erpnext/regional/report/gstr_1/gstr_1.js | 3 +- erpnext/regional/report/gstr_1/gstr_1.py | 102 ++++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index ef2bdb67980..4b98978f130 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = { { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") }, { "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") }, { "value": "EXPORT", "label": __("Export Invoice - 6A") }, - { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") } + { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }, + { "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") } ], "default": "B2B" } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index b095c7293dd..dd1c892fdd8 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -42,6 +42,7 @@ class Gstr1Report(object): shipping_bill_number, shipping_bill_date, reason_for_issuing_document + company_gstin """ def run(self): @@ -63,6 +64,8 @@ class Gstr1Report(object): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() + elif self.filters.get("type_of_business") == "NIL Rated": + self.get_nil_rated_invoices() elif self.invoices: for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) @@ -92,6 +95,57 @@ class Gstr1Report(object): row= [key[0], key[1], value[0], value[1]] self.data.append(row) + def get_nil_rated_invoices(self): + nil_exempt_output = [ + { + "description": "Inter-State supplies to registered persons", + "nil_rate": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Intra-State supplies to registered persons", + "nil_rate": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Inter-State supplies to unregistered persons", + "nil_rate": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + }, + { + "description": "Intra-State supplies to registered persons", + "nil_rate": 0.0, + "exempted": 0.0, + "non_gst": 0.0 + } + ] + + for invoice, details in self.nil_exempt_non_gst.items(): + invoice_detail = self.invoice.get(invoice) + if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): + if is_inter_state(invoice_detail): + nil_exempt_output[0]["nil_rated"] += details[0] + nil_exempt_output[0]["exempted"] += details[1] + nil_exempt_output[0]["non_gst"] += details[2] + else: + nil_exempt_output[1]["nil_rated"] += details[0] + nil_exempt_output[1]["exempted"] += details[1] + nil_exempt_output[1]["non_gst"] += details[2] + else: + if is_inter_state(invoice_detail): + nil_exempt_output[2]["nil_rated"] += details[0] + nil_exempt_output[2]["exempted"] += details[1] + nil_exempt_output[2]["non_gst"] += details[2] + else: + nil_exempt_output[3]["nil_rated"] += details[0] + nil_exempt_output[3]["exempted"] += details[1] + nil_exempt_output[3]["non_gst"] += details[2] + + self.data = nil_exempt_output + def get_b2c_data(self): b2cs_output = {} @@ -241,10 +295,11 @@ class Gstr1Report(object): def get_invoice_items(self): self.invoice_items = frappe._dict() self.item_tax_rate = frappe._dict() + self.nil_exempt_non_gst = {} items = frappe.db.sql(""" - select item_code, parent, taxable_value, base_net_amount, item_tax_rate - from `tab%s Item` + select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, + is_non_gst from `tab%s Item` where parent in (%s) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) @@ -261,6 +316,16 @@ class Gstr1Report(object): tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, []) tax_rate_dict.append(rate) + if d.is_nil_exempt: + self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) + if item_tax_rate: + self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0) + else: + self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0) + elif d.is_non_gst: + self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) + self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0) + def get_items_based_on_tax_rate(self): self.tax_details = frappe.db.sql(""" select @@ -706,6 +771,33 @@ class Gstr1Report(object): "width": 100 } ] + elif self.filters.get("type_of_business") == "NIL Rated": + self.invoice_columns = [ + { + "fieldname": "descripton", + "label": "Description", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "nil_rated", + "label": "Nil Rated", + "fieldtype": "Currency", + "width": 200 + }, + { + "fieldname": "exempted", + "label": "Exempted", + "fieldtype": "Currency", + "width": 200 + }, + { + "fieldname": "non_gst", + "label": "Non GST", + "fieldtype": "Currency", + "width": 200 + } + ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns @@ -1065,3 +1157,9 @@ def download_json_file(): frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' + +def is_inter_state(invoice_detail): + if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: + return True + else: + return False \ No newline at end of file From 103bf197009e7993b74c8a64d2f74476a86de069 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 Jan 2022 22:39:57 +0530 Subject: [PATCH 72/94] fix: JSON for nil/exempt and non gst (cherry picked from commit 55c445cd375a5a16e69205fc702ff86a4577ce48) --- erpnext/regional/report/gstr_1/gstr_1.py | 84 +++++++++++++++++------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index dd1c892fdd8..17b11073346 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -41,7 +41,7 @@ class Gstr1Report(object): port_code, shipping_bill_number, shipping_bill_date, - reason_for_issuing_document + reason_for_issuing_document, company_gstin """ @@ -99,32 +99,32 @@ class Gstr1Report(object): nil_exempt_output = [ { "description": "Inter-State supplies to registered persons", - "nil_rate": 0.0, + "nil_rated": 0.0, "exempted": 0.0, "non_gst": 0.0 }, { "description": "Intra-State supplies to registered persons", - "nil_rate": 0.0, + "nil_rated": 0.0, "exempted": 0.0, "non_gst": 0.0 }, { "description": "Inter-State supplies to unregistered persons", - "nil_rate": 0.0, + "nil_rated": 0.0, "exempted": 0.0, "non_gst": 0.0 }, { - "description": "Intra-State supplies to registered persons", - "nil_rate": 0.0, + "description": "Intra-State supplies to unregistered persons", + "nil_rated": 0.0, "exempted": 0.0, "non_gst": 0.0 } ] for invoice, details in self.nil_exempt_non_gst.items(): - invoice_detail = self.invoice.get(invoice) + invoice_detail = self.invoices.get(invoice) if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): if is_inter_state(invoice_detail): nil_exempt_output[0]["nil_rated"] += details[0] @@ -388,21 +388,24 @@ class Gstr1Report(object): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): - self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - } - ] self.other_columns = [] + self.tax_columns = [] + + if self.filters.get("type_of_business") != "NIL Rated": + self.tax_columns = [ + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Int", + "width": 60 + }, + { + "fieldname": "taxable_value", + "label": "Taxable Value", + "fieldtype": "Currency", + "width": 100 + } + ] if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ @@ -774,10 +777,10 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "NIL Rated": self.invoice_columns = [ { - "fieldname": "descripton", + "fieldname": "description", "label": "Description", "fieldtype": "Data", - "width": 120 + "width": 420 }, { "fieldname": "nil_rated", @@ -861,6 +864,11 @@ def get_json(filters, report_name, data): out = get_advances_json(res, gstin) gst_json["at"] = out + elif filters["type_of_business"] == "NIL Rated": + res = report_data[:-1] + out = get_exempted_json(res) + gst_json["nil"] = out + return { 'report_name': report_name, 'report_type': filters['type_of_business'], @@ -1073,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin): return out +def get_exempted_json(data): + out = { + "inv": [ + { + "sply_ty": "INTRB2B" + }, + { + "sply_ty": "INTRAB2B" + }, + { + "sply_ty": "INTRB2C" + }, + { + "sply_ty": "INTRAB2C" + } + ] + } + + for i, v in enumerate(data): + if data[i].get('nil_rated'): + out['inv'][i]['nil_amt'] = data[i]['nil_rated'] + + if data[i].get('exempted'): + out['inv'][i]['expt_amt'] = data[i]['exempted'] + + if data[i].get('non_gst'): + out['inv'][i]['ngsup_amt'] = data[i]['non_gst'] + + return out + def get_invoice_type_for_cdnr(row): if row.get('gst_category') == 'SEZ': if row.get('export_type') == 'WPAY': From eb3d28fcc048ae67267703a235306e304e41a2a9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 3 Jan 2022 19:40:47 +0530 Subject: [PATCH 73/94] fix: Deferred revenue booking for multi currency invoices via JV (cherry picked from commit 98f294a8ae983fe9bf9b83127fccbf1f1a397ba1) --- erpnext/accounts/deferred_revenue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 7e270601fb5..460d0844a11 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -259,7 +259,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) if not (start_date and end_date): return - account_currency = get_account_currency(item.expense_account) + account_currency = get_account_currency(item.expense_account or item.income_account) if doc.doctype == "Sales Invoice": against, project = doc.customer, doc.project credit_account, debit_account = item.income_account, item.deferred_revenue_account From a08b10d5cf395dea48f77964dc599142d70ac183 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 4 Jan 2022 14:04:18 +0530 Subject: [PATCH 74/94] fix: Add test case for multicurrency invoice (cherry picked from commit 094158f287240fd0807ada6eb89dc6874420aed8) --- erpnext/accounts/deferred_revenue.py | 4 -- .../doctype/journal_entry/journal_entry.py | 24 ++++--- .../sales_invoice/test_sales_invoice.py | 63 +++++++++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 460d0844a11..af6dc577a18 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -407,8 +407,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': credit_account, 'credit': base_amount, 'credit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, @@ -421,8 +419,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': debit_account, 'debit': base_amount, 'debit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 6ec2d4bda03..9c1710217dd 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -397,13 +397,14 @@ class JournalEntry(AccountsController): debit_or_credit = 'Debit' if d.debit else 'Credit' party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, debit_or_credit) + against_voucher = ['', against_voucher[1]] else: if d.reference_type == "Sales Invoice": party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] else: party_account = against_voucher[1] - if (against_voucher[0] != d.party or party_account != d.account): + if (against_voucher[0] != cstr(d.party) or party_account != d.account): frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}") .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], d.reference_type, d.reference_name)) @@ -468,13 +469,22 @@ class JournalEntry(AccountsController): def set_against_account(self): accounts_debited, accounts_credited = [], [] - for d in self.get("accounts"): - if flt(d.debit > 0): accounts_debited.append(d.party or d.account) - if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) + if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'): + for d in self.get('accounts'): + if d.reference_type == 'Sales Invoice': + field = 'customer' + else: + field = 'supplier' - for d in self.get("accounts"): - if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) + else: + for d in self.get("accounts"): + if flt(d.debit > 0): accounts_debited.append(d.party or d.account) + if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) + + for d in self.get("accounts"): + if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) + if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): for d in self.get('accounts'): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e2c1f79bd2e..618acb00aea 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2455,6 +2455,69 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + def test_multi_currency_deferred_revenue_via_journal_entry(self): + deferred_account = create_account(account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", company="_Test Company") + + acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings.book_deferred_entries_via_journal_entry = 1 + acc_settings.submit_journal_entries = 1 + acc_settings.save() + + item = create_item("_Test Item for Deferred Accounting") + item.enable_deferred_expense = 1 + item.deferred_revenue_account = deferred_account + item.save() + + si = create_sales_invoice(customer='_Test Customer USD', currency='USD', + item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True) + + si.set_posting_time = 1 + si.posting_date = '2019-01-01' + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-01" + si.items[0].service_end_date = "2019-03-30" + si.items[0].deferred_expense_account = deferred_account + si.save() + si.submit() + + pda1 = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company" + )) + + pda1.insert() + pda1.submit() + + expected_gle = [ + ["Sales - _TC", 0.0, 2089.89, "2019-01-31"], + [deferred_account, 2089.89, 0.0, "2019-01-31"], + ["Sales - _TC", 0.0, 1887.64, "2019-02-28"], + [deferred_account, 1887.64, 0.0, "2019-02-28"], + ["Sales - _TC", 0.0, 2022.47, "2019-03-15"], + [deferred_account, 2022.47, 0.0, "2019-03-15"] + ] + + gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s + order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.credit) + self.assertEqual(expected_gle[i][2], gle.debit) + self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + + acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings.book_deferred_entries_via_journal_entry = 0 + acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.save() + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From 38a330b55e47cae0ce73d2db364a456e5e9ad76d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 4 Jan 2022 16:24:24 +0530 Subject: [PATCH 75/94] fix: Test case (cherry picked from commit e311667a250d112552a115a4b551efdc53cf6099) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 618acb00aea..76948652f2b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2474,6 +2474,7 @@ class TestSalesInvoice(unittest.TestCase): si.set_posting_time = 1 si.posting_date = '2019-01-01' + si.debit_to = '_Test Receivable USD - _TC' si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-01" si.items[0].service_end_date = "2019-03-30" From a4e897d30bf302f6a11c0cd101828501a3ecb1ab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Jan 2022 19:52:38 +0530 Subject: [PATCH 76/94] fix: Handle frozen books while handling (cherry picked from commit 30a647ff8099105a9fdce02bc98d43969985acb3) --- erpnext/accounts/deferred_revenue.py | 6 ++++++ .../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++-- erpnext/controllers/accounts_controller.py | 2 -- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index af6dc577a18..4b1535e1000 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -255,6 +255,8 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): enable_check = "enable_deferred_revenue" \ if doc.doctype=="Sales Invoice" else "enable_deferred_expense" + accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto') + def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on): start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) if not (start_date and end_date): return @@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): if not amount: return + # check if books nor frozen till endate: + if getdate(end_date) >= getdate(accounts_frozen_upto): + end_date = get_last_day(add_days(accounts_frozen_upto, 1)) + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 76948652f2b..82c5ab5fc1f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2482,6 +2482,9 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() + acc_settings.acc_frozen_upto = '2019-01-31' + acc_settings.save() + pda1 = frappe.get_doc(dict( doctype='Process Deferred Accounting', posting_date=nowdate(), @@ -2495,8 +2498,8 @@ class TestSalesInvoice(unittest.TestCase): pda1.submit() expected_gle = [ - ["Sales - _TC", 0.0, 2089.89, "2019-01-31"], - [deferred_account, 2089.89, 0.0, "2019-01-31"], + ["Sales - _TC", 0.0, 2089.89, "2019-01-28"], + [deferred_account, 2089.89, 0.0, "2019-01-28"], ["Sales - _TC", 0.0, 1887.64, "2019-02-28"], [deferred_account, 1887.64, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 2022.47, "2019-03-15"], @@ -2517,6 +2520,7 @@ class TestSalesInvoice(unittest.TestCase): acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.acc_frozen_upto = None acc_settings.save() def get_sales_invoice_for_e_invoice(): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2a95553fbf2..b7b198eac4c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -191,8 +191,6 @@ class AccountsController(TransactionBase): frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) elif getdate(self.posting_date) > getdate(d.service_end_date): frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) - elif getdate(self.posting_date) > getdate(d.service_start_date): - frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx)) def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() From 8ed138e3733018a2831eb3ef1e4229adaeef4ea1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 7 Jan 2022 20:57:15 +0530 Subject: [PATCH 77/94] fix: Test case (cherry picked from commit 18c4ddadf1428133961f3558d7df8eefb7475f40) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 82c5ab5fc1f..4bf920ac4e4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2520,9 +2520,10 @@ class TestSalesInvoice(unittest.TestCase): acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0 - acc_settings.acc_frozen_upto = None acc_settings.save() + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From eff48299e4310ad02cb494771b5ab84cb6474927 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 Jan 2022 19:44:26 +0530 Subject: [PATCH 78/94] fix: Test case (cherry picked from commit 24bb1e8987c222139884076e4134fe52e3d214f7) --- .../doctype/sales_invoice/test_sales_invoice.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 4bf920ac4e4..d76e655d071 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2459,7 +2459,7 @@ class TestSalesInvoice(unittest.TestCase): deferred_account = create_account(account_name="Deferred Revenue", parent_account="Current Liabilities - _TC", company="_Test Company") - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_single('Accounts Settings') acc_settings.book_deferred_entries_via_journal_entry = 1 acc_settings.submit_journal_entries = 1 acc_settings.save() @@ -2474,7 +2474,7 @@ class TestSalesInvoice(unittest.TestCase): si.set_posting_time = 1 si.posting_date = '2019-01-01' - si.debit_to = '_Test Receivable USD - _TC' + # si.debit_to = '_Test Receivable USD - _TC' si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-01" si.items[0].service_end_date = "2019-03-30" @@ -2482,8 +2482,7 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - acc_settings.acc_frozen_upto = '2019-01-31' - acc_settings.save() + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) pda1 = frappe.get_doc(dict( doctype='Process Deferred Accounting', @@ -2517,12 +2516,12 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_single('Accounts Settings') acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() From 045a505a38f1320736aea8343c287bde4a10e215 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 Jan 2022 23:05:27 +0530 Subject: [PATCH 79/94] fix: Remove comment (cherry picked from commit 08f26de3df28eabb93064217f3a67ff9b0ce01b9) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d76e655d071..7a26611b58d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2474,7 +2474,7 @@ class TestSalesInvoice(unittest.TestCase): si.set_posting_time = 1 si.posting_date = '2019-01-01' - # si.debit_to = '_Test Receivable USD - _TC' + si.debit_to = '_Test Receivable USD - _TC' si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-01" si.items[0].service_end_date = "2019-03-30" @@ -2521,7 +2521,7 @@ class TestSalesInvoice(unittest.TestCase): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) + frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() From ef3e4ee61feffc03b3270c0a18198298ac5917cc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 15 Jan 2022 18:27:40 +0530 Subject: [PATCH 80/94] fix: Remove unwanted test (cherry picked from commit fd922f1617303d8bf26529b9a9e9d3e919cba011) --- .../sales_invoice/test_sales_invoice.py | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7a26611b58d..12e00dcac6f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1789,47 +1789,6 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_revenue_post_account_freeze_upto_by_admin(self): - frappe.set_user("Administrator") - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) - - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") - - item = create_item("_Test Item for Deferred Accounting") - item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account - item.no_of_months = 12 - item.save() - - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True) - si.items[0].enable_deferred_revenue = 1 - si.items[0].service_start_date = "2019-01-10" - si.items[0].service_end_date = "2019-03-15" - si.items[0].deferred_revenue_account = deferred_account - si.save() - si.submit() - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager') - - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) - - pda1.insert() - self.assertRaises(frappe.ValidationError, pda1.submit) - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) - def test_fixed_deferred_revenue(self): deferred_account = create_account(account_name="Deferred Revenue", parent_account="Current Liabilities - _TC", company="_Test Company") From ecbd885aad5de796380a790ea991b8b7c10dd8d2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 16 Jan 2022 12:33:55 +0530 Subject: [PATCH 81/94] fix: exclude existing serial numbers while auto creating new #29292 (#29300) fix: exclude existing serial numbers while auto creating new (cherry picked from commit 2b681f04fd47b0f73685c8680dcaa7203d671be3) Co-authored-by: Dany Robert --- erpnext/stock/doctype/serial_no/serial_no.py | 8 +++++++- .../stock/doctype/serial_no/test_serial_no.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b2321acf9e8..3b325b80295 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -421,10 +421,16 @@ def update_serial_nos(sle, item_det): def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] for i in range(cint(qty)): - serial_nos.append(make_autoname(serial_no_series, "Serial No")) + serial_nos.append(get_new_serial_number(serial_no_series)) return "\n".join(serial_nos) +def get_new_serial_number(series): + sr_no = make_autoname(series, "Serial No") + if frappe.db.exists("Serial No", sr_no): + sr_no = get_new_serial_number(series) + return sr_no + def auto_make_serial_nos(args): serial_nos = get_serial_nos(args.get('serial_no')) created_numbers = [] diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 99000d12016..9cdc0f74355 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -8,6 +8,7 @@ import frappe from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -176,6 +177,24 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) + def test_auto_creation_of_serial_no(self): + """ + Test if auto created Serial No excludes existing serial numbers + """ + item_code = make_item("_Test Auto Serial Item ", { + "has_serial_no": 1, + "serial_no_series": "XYZ.###" + }).item_code + + # Reserve XYZ005 + pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005") + # XYZ005 is already used and will throw an error if used again + pr_2 = make_purchase_receipt(item_code=item_code, qty=10) + + self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005") + for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no): + self.assertNotEqual(serial_no, "XYZ005") + def test_serial_no_sanitation(self): "Test if Serial No input is sanitised before entering the DB." item_code = "_Test Serialized Item" From 42d3b0c0634e8e48c9535d7a4c43244f9503e0d1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 16 Jan 2022 14:05:09 +0530 Subject: [PATCH 82/94] fix: only add stock queue if FIFO (#29302) (#29305) (cherry picked from commit b0cf6195e9efe01150c759483c8ec1c465c746a6) Co-authored-by: Ankush Menat --- erpnext/stock/stock_ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 456cfe3d76f..2ce7253b8cc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -441,8 +441,9 @@ class update_entries_after(object): # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + if self.valuation_method != "Moving Average": + self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) From 5db8af8ceef4c1df0472fd247036a2116344d2b7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 16 Jan 2022 20:59:19 +0530 Subject: [PATCH 83/94] fix(patch): sle.serial_no = "\n" causes incorrect queue (#29306) (#29309) This happens due to old data. (cherry picked from commit 66bf21f14337a9c83b1ab423a7fafe70781ef21b) Co-authored-by: Ankush Menat --- erpnext/patches.txt | 2 +- erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py | 4 +++- erpnext/stock/stock_ledger.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 18319f70c47..8c9b365fa7e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -305,7 +305,7 @@ erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.remove_bad_selling_defaults -erpnext.patches.v13_0.trim_whitespace_from_serial_nos +erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022 erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index 8a9633d8964..4ec22e9d0e1 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -9,13 +9,15 @@ def execute(): from `tabStock Ledger Entry` where is_cancelled = 0 - and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s) + and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s + or serial_no = %s ) """, ( " %", # leading whitespace "% ", # trailing whitespace "%\n %", # leading whitespace on newline "% \n%", # trailing whitespace on newline + "\n", # just new line ), as_dict=True, ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2ce7253b8cc..85170553560 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -105,6 +105,7 @@ def get_args_for_future_sle(row): def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + for sn in get_serial_nos(sle.serial_no): args = copy.deepcopy(sle) args.serial_no = sn @@ -415,6 +416,8 @@ class update_entries_after(object): return sorted(entries_to_fix, key=lambda k: k['timestamp']) def process_sle(self, sle): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + # previous sle data for this warehouse self.wh_data = self.data[sle.warehouse] @@ -429,7 +432,7 @@ class update_entries_after(object): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if sle.serial_no: + if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) if sle.voucher_type == "Stock Reconciliation": From de9a0d6a7fead8d7156d750b65e7ea64e7d9971d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 16 Jan 2022 21:44:07 +0530 Subject: [PATCH 84/94] fix: ignore cancelled SLEs (#29303) (#29307) (cherry picked from commit 82ea95873049deff46930ea0a792df7609fdf39f) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/batch/batch.py | 1 + .../batch_item_expiry_status/batch_item_expiry_status.py | 3 ++- erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py | 2 +- .../itemwise_recommended_reorder_level.py | 1 + erpnext/stock/stock_ledger.py | 3 +++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 2fcdba59e35..c0cb30b5edb 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -293,6 +293,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): join `tabStock Ledger Entry` ignore index (item_code, warehouse) on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s + and `tabStock Ledger Entry`.is_cancelled = 0 and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 44e13869ddb..87097c72fa4 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters): return frappe.db.sql("""select item_code, batch_no, warehouse, posting_date, actual_qty from `tabStock Ledger Entry` - where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % + where is_cancelled = 0 + and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % conditions, as_dict=1) def get_item_warehouse_batch_map(filters, float_precision): diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 5f6184d6f35..058af77aa21 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] svd_list = frappe.get_list( 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], - filters=[('voucher_no', 'in', voucher_nos)] + filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)] ) assign_item_groups_to_svd_list(svd_list) return svd_list diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 3f490653e14..cfa1e474c7b 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -76,6 +76,7 @@ def get_consumed_items(condition): on sle.voucher_no = se.name where actual_qty < 0 + and is_cancelled = 0 and voucher_type not in ('Delivery Note', 'Sales Invoice') %s group by item_code""" % condition, as_dict=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 85170553560..3008e822a89 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -643,6 +643,7 @@ class update_entries_after(object): where company = %s and actual_qty > 0 + and is_cancelled = 0 and (serial_no = %s or serial_no like %s or serial_no like %s @@ -946,6 +947,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, item_code = %s AND warehouse = %s AND valuation_rate >= 0 + AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) @@ -956,6 +958,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, where item_code = %s AND valuation_rate > 0 + AND is_cancelled = 0 AND NOT(voucher_no = %s AND voucher_type = %s) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) From 83ca2422f6d6260259d4dc31c427984ea3620360 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 12:08:21 +0530 Subject: [PATCH 85/94] fix: show work order progress bar even it is closed (#29312) (#29313) (cherry picked from commit 4d480358289af007618a12e4b74c963d0c1524cb) Co-authored-by: Anupam Kumar --- erpnext/manufacturing/doctype/work_order/work_order.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index e47103599e9..3f2f39e73af 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", { erpnext.work_order.set_custom_buttons(frm); frm.set_intro(""); - if (frm.doc.docstatus === 0 && !frm.doc.__islocal) { + if (frm.doc.docstatus === 0 && !frm.is_new()) { frm.set_intro(__("Submit this Work Order for further processing.")); + } else { + frm.trigger("show_progress_for_items"); + frm.trigger("show_progress_for_operations"); } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus===1) { - frm.trigger('show_progress_for_items'); - frm.trigger('show_progress_for_operations'); - } - if (frm.doc.docstatus === 1 && frm.doc.operations && frm.doc.operations.length) { From 3b0f80b8657a571c72652f42ad945646c9c3e257 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 7 Jan 2022 11:10:23 +0530 Subject: [PATCH 86/94] fix: dont update sle values from get_gl_entries (cherry picked from commit 8f5772463c7f1188110caccae0906d0e98a4b050) --- erpnext/controllers/stock_controller.py | 33 +------------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 7073e32f536..f22669b2555 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import ( from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map -from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate +from erpnext.stock.stock_ledger import get_items_to_be_repost class QualityInspectionRequiredError(frappe.ValidationError): pass @@ -111,17 +111,6 @@ class StockController(AccountsController): self.check_expense_account(item_row) - # If the item does not have the allow zero valuation rate flag set - # and ( valuation rate not mentioned in an incoming entry - # or incoming entry not found while delivering the item), - # try to pick valuation rate from previous sle or Item master and update in SLE - # Otherwise, throw an exception - - if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \ - and not item_row.get("allow_zero_valuation_rate"): - - sle = self.update_stock_ledger_entries(sle) - # expense account/ target_warehouse / source_warehouse if item_row.get('target_warehouse'): warehouse = item_row.get('target_warehouse') @@ -164,26 +153,6 @@ class StockController(AccountsController): return frappe.flags.debit_field_precision - def update_stock_ledger_entries(self, sle): - sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - self.doctype, self.name, currency=self.company_currency, company=self.company) - - sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate) - sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate) - - if sle.name: - frappe.db.sql(""" - update - `tabStock Ledger Entry` - set - stock_value = %(stock_value)s, - valuation_rate = %(valuation_rate)s, - stock_value_difference = %(stock_value_difference)s - where - name = %(name)s""", (sle)) - - return sle - def get_voucher_details(self, default_expense_account, default_cost_center, sle_map): if self.doctype == "Stock Reconciliation": reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose") From a0c2c94cff2c99d632c632b62ebd2a1d7d7d9440 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 16 Jan 2022 13:24:40 +0530 Subject: [PATCH 87/94] test: make sure zero incoming rate is maintained while consuming (cherry picked from commit 0272397e54513b5a8c18f4e817d3ddf708efc191) --- .../doctype/stock_entry/test_stock_entry.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index af7acc36353..3d936c50f7a 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -852,6 +852,34 @@ class TestStockEntry(ERPNextTestCase): self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) + def test_zero_incoming_rate(self): + """ Make sure incoming rate of 0 is allowed while consuming. + + qty | rate | valuation rate + 1 | 100 | 100 + 1 | 0 | 50 + -1 | 100 | 0 + -1 | 0 <--- assert this + """ + item_code = "_TestZeroVal" + warehouse = "_Test Warehouse - _TC" + create_item('_TestZeroVal') + _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100) + receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True) + receipt2.items[0].allow_zero_valuation_rate = 1 + receipt2.save() + receipt2.submit() + + issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) + + value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + self.assertEqual(value_diff, -100) + + issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) + value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + self.assertEqual(value_diff, 0) + + def test_gle_for_opening_stock_entry(self): mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=50, basic_rate=100, From ea9756da8fbdd270e8f10da5f48bbb5e21215da0 Mon Sep 17 00:00:00 2001 From: ruthra Date: Thu, 6 Jan 2022 11:34:47 +0530 Subject: [PATCH 88/94] fix: get project from PO into payment entry (cherry picked from commit ca17c7226ce343e63cf929023babee425e3da3c5) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c8c214500c9..e3c9a18dccd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3,6 +3,7 @@ import json +from functools import reduce import frappe from frappe import ValidationError, _, scrub, throw @@ -1523,6 +1524,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") + if dt == 'Purchase Order': + pe.project = reduce(lambda prev,cur: prev or cur, [x.project for x in doc.get('items')], None) # get first non-empty project from items if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) From 7f5415177e88d1968c59fc23ed2684aae2af7538 Mon Sep 17 00:00:00 2001 From: ruthra Date: Mon, 10 Jan 2022 12:58:58 +0530 Subject: [PATCH 89/94] refactor: get project from basic transactions - sales order, sales invoice, purchase order and purchase order - if project not found in transaction, get from items (cherry picked from commit 8026f86548d0eccc56b1f66e72f47719e7a85969) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e3c9a18dccd..96ebc5e0658 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1524,8 +1524,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.paid_amount = paid_amount pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") - if dt == 'Purchase Order': - pe.project = reduce(lambda prev,cur: prev or cur, [x.project for x in doc.get('items')], None) # get first non-empty project from items + + if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']: + pe.project = (doc.get('project') or + reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) From 5da7e28d20b4bdc3b01a26235b593a199b3dc5db Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 17 Jan 2022 15:01:17 +0530 Subject: [PATCH 90/94] fix: Linting issues (cherry picked from commit 09172002e729a2bbee2362f3132d0cbd688c6b20) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 96ebc5e0658..80f61d4f02e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1527,7 +1527,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']: pe.project = (doc.get('project') or - reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items + reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) From c2d58ba796ca053fa4f2660c4a405a6c23d540b8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 16:48:04 +0530 Subject: [PATCH 91/94] feat(UX): Option to exclude holidays while marking monthly attendance (backport #29185) (#29321) (cherry picked from commit 6aac8de53e5ec57fea3dbdb21d03a6dbc0eade62) Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/attendance/attendance.py | 11 ++++-- .../hr/doctype/attendance/attendance_list.js | 36 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 7dcfac249f4..b1eaaf8b587 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -5,9 +5,9 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate +from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate -from erpnext.hr.utils import validate_active_employee +from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee class Attendance(Document): @@ -171,7 +171,7 @@ def get_month_map(): }) @frappe.whitelist() -def get_unmarked_days(employee, month): +def get_unmarked_days(employee, month, exclude_holidays=0): import calendar month_map = get_month_map() @@ -191,6 +191,11 @@ def get_unmarked_days(employee, month): ]) marked_days = [get_datetime(record.attendance_date) for record in records] + if cint(exclude_holidays): + holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) + holidays = [get_datetime(record) for record in holiday_dates] + marked_days.extend(holidays) + unmarked_days = [] for date in dates_of_month: diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 6b3c29a76b4..3a5c5915396 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = { onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("status", "hidden", 1); + dialog.set_df_property("exclude_holidays", "hidden", 1); dialog.set_df_property("month", "value", ''); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; @@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = { onchange: function() { if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("exclude_holidays", "hidden", 0); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; - me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => { + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { if (options.length > 0) { dialog.set_df_property("unmarked_days", "hidden", 0); dialog.set_df_property("unmarked_days", "options", options); @@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = { reqd: 1, }, + { + label: __("Exclude Holidays"), + fieldtype: "Check", + fieldname: "exclude_holidays", + hidden: 1, + onchange: function() { + if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { + dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { + if (options.length > 0) { + dialog.set_df_property("unmarked_days", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", options); + } else { + dialog.no_unmarked_days_left = true; + } + }); + } + } + }, { label: __("Unmarked Attendance for days"), fieldname: "unmarked_days", @@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = { }); }, - get_multi_select_options: function(employee, month) { + get_multi_select_options: function(employee, month, exclude_holidays) { return new Promise(resolve => { frappe.call({ method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', @@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = { args: { employee: employee, month: month, + exclude_holidays: exclude_holidays } }).then(r => { var options = []; From a3a99207711698094302f8f3d7de11a2c808d09e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 21 Dec 2021 16:49:41 +0530 Subject: [PATCH 92/94] fix: incorrect incoming rate computation for sr no (cherry picked from commit b9642a1036d7b6fa9a49a08b64145bfc2f56916f) --- erpnext/stock/stock_ledger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3008e822a89..f43d82d8b59 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -599,9 +599,9 @@ class update_entries_after(object): incoming_rate = self.wh_data.valuation_rate stock_value_change = 0 - if incoming_rate: + if actual_qty > 0: stock_value_change = actual_qty * incoming_rate - elif actual_qty < 0: + else: # In case of delivery/stock issue, get average purchase rate # of serial nos of current entry if not sle.is_cancelled: From 9a3a67962884eeace554e9b798de0748d33ba277 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 22 Dec 2021 12:38:51 +0530 Subject: [PATCH 93/94] fix: get incoming rate only if not return (cherry picked from commit fcc885bc16403c80f27e6bfb970b156fd2b636b6) --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index cc773b75963..4ff851d7f94 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -385,7 +385,7 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - if not d.incoming_rate: + if not (self.get("is_return") and d.incoming_rate): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, From 0411bcbd5c33af3e2bc9ef6212a5c8eb8a67e0fb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 16 Jan 2022 17:45:27 +0530 Subject: [PATCH 94/94] test: serial no valuation test case (cherry picked from commit 5d27a7672e73e514e82fd79b960b16479403e696) --- .../stock/doctype/serial_no/test_serial_no.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 9cdc0f74355..f8cea717251 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -11,6 +11,7 @@ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delive from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -22,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase class TestSerialNo(ERPNextTestCase): + + def tearDown(self): + frappe.db.rollback() + def test_cannot_create_direct(self): frappe.delete_doc_if_exists("Serial No", "_TCSER0001") @@ -211,7 +216,28 @@ class TestSerialNo(ERPNextTestCase): self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") - frappe.db.rollback() + def test_correct_serial_no_incoming_rate(self): + """ Check correct consumption rate based on serial no record. + """ + item_code = "_Test Serialized Item" + warehouse = "_Test Warehouse - _TC" + serial_nos = ["LOWVALUATION", "HIGHVALUATION"] + + in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, + serial_no=serial_nos[0]) + in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, + serial_no=serial_nos[1]) + + out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) + + # change serial no + out.items[0].serial_no = serial_nos[1] + out.save() + out.submit() + + value_diff = frappe.db.get_value("Stock Ledger Entry", + {"voucher_no": out.name, "voucher_type": "Delivery Note"}, + "stock_value_difference" + ) + self.assertEqual(value_diff, -113) - def tearDown(self): - frappe.db.rollback()