From 8dd26949b76b8762fa4f5a1b825bf052ba248f5d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Oct 2023 17:12:47 +0530 Subject: [PATCH 01/40] refactor: for non-repost fields, don't validate (cherry picked from commit c1782c50158e50e1e57e7eeda276ffd46203f86c) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f6ec446ef35..c5e45d4f3a9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -546,8 +546,9 @@ class PurchaseInvoice(BuyingController): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 03aca8ad588..7f124f541f3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -548,8 +548,9 @@ class SalesInvoice(SellingController): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def set_paid_amount(self): paid_amount = 0.0 From d37a1811db239657dc1517501105918bbc4fb14d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 14:42:23 +0530 Subject: [PATCH 02/40] refactor: add validation for Advances in SI/PI (cherry picked from commit 0cdd6435a556309d62240fc669ce431efee040c3) --- erpnext/controllers/accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7207743e095..ab20f82ea25 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -12,6 +12,7 @@ from frappe.utils import ( add_days, add_months, cint, + comma_and, flt, fmt_money, formatdate, @@ -180,6 +181,17 @@ class AccountsController(TransactionBase): self.validate_party_account_currency() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: + if invalid_advances := [ + x for x in self.advances if not x.reference_type or not x.reference_name + ]: + frappe.throw( + _( + "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." + ).format( + frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments")) + ) + ) + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() From f9b2355066b04489b7f4a69270eeac5032d5c32c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Oct 2023 23:45:58 +0200 Subject: [PATCH 03/40] fix: german tranlations of "Is Return" (cherry picked from commit 38ca164662532a97469db4b2d0c1519a570120eb) --- erpnext/translations/de.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index dcba85b4d20..0c840f44ea8 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -5007,7 +5007,7 @@ ACC-PINV-.YYYY.-,ACC-PINV-.JJJJ.-, Tax Withholding Category,Steuereinbehalt Kategorie, Edit Posting Date and Time,Buchungsdatum und -uhrzeit bearbeiten, Is Paid,Ist bezahlt, -Is Return (Debit Note),ist Rücklieferung (Lastschrift), +Is Return (Debit Note),Ist Rechnungskorrektur (Retoure), Apply Tax Withholding Amount,Steuereinbehaltungsbetrag anwenden, Accounting Dimensions ,Buchhaltung Dimensionen, Supplier Invoice Details,Lieferant Rechnungsdetails, @@ -5133,7 +5133,7 @@ Default Bank / Cash account will be automatically updated in Salary Journal Entr ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-, Include Payment (POS),(POS) Zahlung einschließen, Offline POS Name,Offline-Verkaufsstellen-Name, -Is Return (Credit Note),ist Rücklieferung (Gutschrift), +Is Return (Credit Note),Ist Rechnungskorrektur (Retoure), Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag, Customer PO Details,Auftragsdetails, Customer's Purchase Order,Bestellung des Kunden, @@ -7993,7 +7993,7 @@ Customs Tariff Number,Zolltarifnummer, Tariff Number,Tarifnummer, Delivery To,Lieferung an, MAT-DN-.YYYY.-,MAT-DN-.YYYY.-, -Is Return,Ist Rückgabe, +Is Return,Ist Retoure, Issue Credit Note,Gutschrift ausgeben, Return Against Delivery Note,Zurück zum Lieferschein, Customer's Purchase Order No,Bestellnummer des Kunden, From 0590f21814c189df07d87bc08a33c9d4fed4b95e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:44:36 +0530 Subject: [PATCH 04/40] fix: don't set finance books if gross_purchase_amount is not set (backport #37480) (#37482) fix: don't set finance books if gross_purchase_amount is not set (#37480) (cherry picked from commit 18e3a8907a78c323d1aacce6d46732f97ee8fdd2) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 0605189fec0..5d7794e1bcc 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -284,7 +284,7 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code && frm.doc.calculate_depreciation) { + if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { frm.trigger('set_finance_book'); } else { frm.set_value('finance_books', []); @@ -448,7 +448,7 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); - if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { frm.trigger("set_finance_book"); } else { frm.set_value("finance_books", []); From cf0ab51348fdc9e7266004b3caf2de9fa4025acb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Oct 2023 20:43:15 +0530 Subject: [PATCH 05/40] refactor(patch): ignore links on closing balance patch (cherry picked from commit 17ca8756a72765e10e17d2a2b81f29129263ab26) --- .../doctype/account_closing_balance/account_closing_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py index e75af7047f1..d06bd833c8b 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date): } ) cle.flags.ignore_permissions = True + cle.flags.ignore_links = True cle.submit() From d2b22db5001ae6544e872234c6c3434f24c5a6b1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 13 Oct 2023 10:22:20 +0530 Subject: [PATCH 06/40] fix: use `flt` to ignore TypeError (#37481) --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 13bbe1f5c08..84bcb99f738 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -699,7 +699,7 @@ class update_entries_after(object): ) if self.valuation_method == "Moving Average": - rate = self.data[self.args.warehouse].previous_sle.valuation_rate + rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate) else: rate = get_rate_for_return( sle.voucher_type, From d266423011ff4c16f3e12231df34ffeeed56cf62 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 10 Oct 2023 10:30:09 +0000 Subject: [PATCH 07/40] fix(gp): wrong `allocated_amount` on multi sales person invoice (cherry picked from commit bda82bf1e9622dda9f7fa42b27b31b3879de342b) --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2bfb4105c1d..de3d57d095a 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -544,6 +544,8 @@ class GrossProfitGenerator(object): new_row.qty += flt(row.qty) new_row.buying_amount += flt(row.buying_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision) + if self.filters.get("group_by") == "Sales Person": + new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) From 6f143d35aab7258832acf8ded0ab42d339e33dc0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Oct 2023 15:39:54 +0530 Subject: [PATCH 08/40] fix: keyerror on gl and pl comparision report (cherry picked from commit ad00df0af6556a806e3b9b40ecaac719f0ce20a4) --- .../general_and_payment_ledger_comparison.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 553c137f024..099884a48ec 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object): self.gle_balances = set(val.gle) | self.gle_balances self.ple_balances = set(val.ple) | self.ple_balances - self.diff1 = self.gle_balances.difference(self.ple_balances) - self.diff2 = self.ple_balances.difference(self.gle_balances) + self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances) + self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances) self.diff = frappe._dict({}) - for x in self.diff1: + for x in self.variation_in_payment_ledger: self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) - for x in self.diff2: - self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]})) + for x in self.variation_in_general_ledger: + self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update( + frappe._dict({"pl_balance": x[4]}) + ) def generate_data(self): self.data = [] From 9406ddbff08d895a01aa2b9cd132879bea3a0fbc Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 14 Oct 2023 16:53:29 +0530 Subject: [PATCH 09/40] fix: Stock Reconciliation Insufficient Stock Error (#37494) * fix: Stock Reconciliation Insufficient Stock Error * fix: linter * test: add test case for Stock Reco Batch Item --- .../test_stock_reconciliation.py | 52 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 44 +++++++++------- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index d1e5c5d345f..c913af3301a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -956,6 +956,58 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertRaises(frappe.ValidationError, sr.save) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_backdated_stock_reco_for_batch_item_dont_have_future_sle(self): + # Step - 1: Create a Batch Item + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.###", + } + ).name + + # Step - 2: Create Opening Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=10, + purpose="Opening Stock", + posting_date=add_days(nowdate(), -2), + ) + + # Step - 3: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se1 = make_stock_entry( + item_code=item, + target="_Test Warehouse - _TC", + qty=100, + ) + + # Step - 4: Create Stock Entry (Material Issue) + make_stock_entry( + item_code=item, + source="_Test Warehouse - _TC", + qty=100, + batch_no=se1.items[0].batch_no, + purpose="Material Issue", + ) + + # Step - 5: Create Stock Reconciliation (Backdated) after the Stock Reconciliation 1 (Step - 2) + sr2 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=5, + batch_no=sr1.items[0].batch_no, + posting_date=add_days(nowdate(), -1), + ) + + self.assertEqual(sr2.docstatus, 1) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 84bcb99f738..bdae87c7ee7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1617,27 +1617,33 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 -def get_future_sle_with_negative_qty(args): - return frappe.db.sql( - """ - select - qty_after_transaction, posting_date, posting_time, - voucher_type, voucher_no - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and voucher_no != %(voucher_no)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) - and is_cancelled = 0 - and qty_after_transaction < 0 - order by timestamp(posting_date, posting_time) asc - limit 1 - """, - args, - as_dict=1, +def get_future_sle_with_negative_qty(sle): + SLE = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(SLE) + .select( + SLE.qty_after_transaction, SLE.posting_date, SLE.posting_time, SLE.voucher_type, SLE.voucher_no + ) + .where( + (SLE.item_code == sle.item_code) + & (SLE.warehouse == sle.warehouse) + & (SLE.voucher_no != sle.voucher_no) + & ( + CombineDatetime(SLE.posting_date, SLE.posting_time) + >= CombineDatetime(sle.posting_date, sle.posting_time) + ) + & (SLE.is_cancelled == 0) + & (SLE.qty_after_transaction < 0) + ) + .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) + .limit(1) ) + if sle.voucher_type == "Stock Reconciliation" and sle.batch_no: + query = query.where(SLE.batch_no == sle.batch_no) + + return query.run(as_dict=True) + def get_future_sle_with_negative_batch_qty(args): return frappe.db.sql( From f1814a1a2a1c82c83c0f04fcc27b3b92526dd906 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 09:57:39 +0530 Subject: [PATCH 10/40] fix: serial and batch no get removed on save of return DN (#37476) * fix: serial and batch no get removed on save of return DN * test: add test case for DN return with product bundle --- .../doctype/delivery_note/delivery_note.py | 12 ++++ .../delivery_note/test_delivery_note.py | 55 +++++++++++++++++-- .../doctype/packed_item/packed_item.json | 3 +- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 115827a60cb..b18ee9943c7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -144,6 +144,7 @@ class DeliveryNote(SellingController): from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + self.set_product_bundle_reference_in_packed_items() # should be called before `make_packing_list` make_packing_list(self) if self._action != "submit" and not self.is_return: @@ -430,6 +431,17 @@ class DeliveryNote(SellingController): else: serial_nos.append(serial_no) + def set_product_bundle_reference_in_packed_items(self): + if self.packed_items and ((self.is_return and self.return_against) or self.amended_from): + if items_ref_map := { + item.dn_detail or item.get("_amended_from"): item.name + for item in self.items + if item.dn_detail or item.get("_amended_from") + }: + for item in self.packed_items: + if item.parent_detail_docname in items_ref_map: + item.parent_detail_docname = items_ref_map[item.parent_detail_docname] + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 093e16c1cf8..8ea87f00c5b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -10,6 +10,7 @@ from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on +from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.test_sales_order import ( automatically_fetch_payment_terms, @@ -268,8 +269,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.items[0].returned_qty, 2) self.assertEqual(dn.per_returned, 40) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - return_dn_2 = make_return_doc("Delivery Note", dn.name) # Check if unreturned amount is mapped in 2nd return @@ -361,8 +360,6 @@ class TestDeliveryNote(FrappeTestCase): dn.submit() self.assertEqual(dn.items[0].incoming_rate, 150) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - return_dn = make_return_doc(dn.doctype, dn.name) return_dn.items[0].warehouse = return_warehouse return_dn.save().submit() @@ -1182,7 +1179,6 @@ class TestDeliveryNote(FrappeTestCase): ) def test_batch_expiry_for_delivery_note(self): - from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt item = make_item( @@ -1239,6 +1235,55 @@ class TestDeliveryNote(FrappeTestCase): # Test - 1: ValidationError should be raised self.assertRaises(frappe.ValidationError, dn.submit) + def test_packed_items_for_return_delivery_note(self): + # Step - 1: Create Items + product_bundle_item = make_item(properties={"is_stock_item": 0}).name + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.#####", + } + ).name + serial_item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.#####"} + ).name + + # Step - 2: Inward Stock + se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3) + serial_nos = ( + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3) + .items[0] + .serial_no + ) + + # Step - 3: Create a Product Bundle + from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import ( + create_product_bundle_item, + ) + + create_product_bundle_item(product_bundle_item, packed_items=[[batch_item, 1], [serial_item, 1]]) + + # Step - 4: Create a Delivery Note for the Product Bundle + dn = create_delivery_note( + item_code=product_bundle_item, + warehouse="_Test Warehouse - _TC", + qty=3, + do_not_submit=True, + ) + dn.packed_items[1].serial_no = serial_nos + dn.save() + dn.submit() + + # Step - 5: Create a Return Delivery Note(Sales Return) + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save() + return_dn.submit() + + self.assertEqual(return_dn.packed_items[0].batch_no, dn.packed_items[0].batch_no) + self.assertEqual(return_dn.packed_items[1].serial_no, dn.packed_items[1].serial_no) + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index c5fb2411c28..679c6c149e9 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -192,7 +192,6 @@ "fieldtype": "Data", "hidden": 1, "label": "Parent Detail docname", - "no_copy": 1, "oldfieldname": "parent_detail_docname", "oldfieldtype": "Data", "print_hide": 1, @@ -259,7 +258,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-28 13:16:38.460806", + "modified": "2023-10-14 23:26:11.755425", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 405d1528c3d8dcff120003ffaacaf24d429c806a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 09:28:42 +0530 Subject: [PATCH 11/40] test: use fixtures for sales and purchase invoice (cherry picked from commit c322e5f38140b1fab8f940db542e25c2b122ab54) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 9 ++++++++- 1 file changed, 8 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 e0a7ff002bb..55fecc560e3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,8 +6,12 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map +<<<<<<< HEAD from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings +======= +from frappe.tests.utils import FrappeTestCase, change_settings +>>>>>>> c322e5f381 (test: use fixtures for sales and purchase invoice) from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext @@ -38,7 +42,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import from erpnext.stock.utils import get_incoming_rate, get_stock_balance -class TestSalesInvoice(unittest.TestCase): +class TestSalesInvoice(FrappeTestCase): def setUp(self): from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items @@ -46,6 +50,9 @@ class TestSalesInvoice(unittest.TestCase): create_internal_parties() setup_accounts() + def tearDown(self): + frappe.db.rollback() + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From 9d6b434d1f30acac9315cdbeac75266158b0998d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 10:56:39 +0530 Subject: [PATCH 12/40] refactor(test): unset accounts frozen date (cherry picked from commit fc50b174eb5f37a40b522f7e073d11260e0c12c8) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 55fecc560e3..ca6d83f4cef 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,10 +49,14 @@ class TestSalesInvoice(FrappeTestCase): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() + def remove_accounts_frozen_date(self): + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From 485cb7dd280b3f218c3bdf5ef723a36a0f9dbb67 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 13:12:21 +0530 Subject: [PATCH 13/40] refactor(test): use @change_settings in sales invoice (cherry picked from commit 58065f31b1e2e550661a47b4442f6861406ebec5) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ca6d83f4cef..08d991c0035 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,14 +49,10 @@ class TestSalesInvoice(FrappeTestCase): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() - self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() - def remove_accounts_frozen_date(self): - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 @@ -3083,8 +3079,12 @@ class TestSalesInvoice(FrappeTestCase): si.commission_rate = commission_rate self.assertRaises(frappe.ValidationError, si.save) + @change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)}) def test_sales_invoice_submission_post_account_freezing_date(self): +<<<<<<< HEAD frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) +======= +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -3093,8 +3093,11 @@ class TestSalesInvoice(FrappeTestCase): si.posting_date = getdate() si.submit() +<<<<<<< HEAD frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) +======= +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3123,6 +3126,13 @@ class TestSalesInvoice(FrappeTestCase): frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance) + @change_settings( + "Accounts Settings", + { + "book_deferred_entries_via_journal_entry": 1, + "submit_journal_entries": 1, + }, + ) def test_multi_currency_deferred_revenue_via_journal_entry(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -3130,11 +3140,6 @@ class TestSalesInvoice(FrappeTestCase): company="_Test Company", ) - 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() - item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_expense = 1 item.item_defaults[0].deferred_revenue_account = deferred_account @@ -3200,12 +3205,16 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) +<<<<<<< HEAD acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entries = 0 acc_settings.save() frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) +======= + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( From 91a5bd86151e72e93e2ab8db86aaa718f169a6f1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 14:57:31 +0530 Subject: [PATCH 14/40] refactor(test): fix broken test cases in Sales Invoice (cherry picked from commit 8ebe5733ac61b6291b22901dbbf070093200706f) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 08d991c0035..528fc6f6c26 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,6 +49,7 @@ class TestSalesInvoice(FrappeTestCase): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) def tearDown(self): frappe.db.rollback() @@ -3205,6 +3206,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) +<<<<<<< HEAD <<<<<<< HEAD acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 @@ -3216,6 +3218,8 @@ class TestSalesInvoice(FrappeTestCase): frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) >>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) +======= +>>>>>>> 8ebe5733ac (refactor(test): fix broken test cases in Sales Invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 From 5699a8daa29c67e2e219838d25ed7f7813efda62 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 20:18:59 +0530 Subject: [PATCH 15/40] refactor(test): use @change_settings to fix failing test cases (cherry picked from commit de9baef84a77f5bb2aa94f200147d0689462b9c3) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 528fc6f6c26..6aae93388d8 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -180,6 +180,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_payment_entry_unlink_against_standalone_credit_note(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1301,6 +1302,7 @@ class TestSalesInvoice(FrappeTestCase): dn.submit() return dn + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_sales_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, From b97fdbe6fce93ec52e688a2be692a254dc13f651 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 05:56:52 +0530 Subject: [PATCH 16/40] refactor(test): use test fixture in subscription (cherry picked from commit 3bdf4f628c40d4e8ac19a41c738a8ba382d90d99) # Conflicts: # erpnext/accounts/doctype/subscription/test_subscription.py --- .../doctype/subscription/test_subscription.py | 525 ++++++++++++++++++ 1 file changed, 525 insertions(+) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index c911e7fe12d..4acddfec7e2 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils.data import ( add_days, add_months, @@ -19,8 +20,532 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") +<<<<<<< HEAD def create_plan(): if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): +======= +class TestSubscription(FrappeTestCase): + def setUp(self): + make_plans() + create_parties() + reset_settings() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() + + def test_create_subscription_with_trial_with_correct_period(self): + subscription = create_subscription( + trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1) + ) + self.assertEqual(subscription.trial_period_start, nowdate()) + self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) + self.assertEqual( + add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) + ) + self.assertEqual( + add_to_date(subscription.current_invoice_start, months=1, days=-1), + get_date_str(subscription.current_invoice_end), + ) + self.assertEqual(subscription.invoices, []) + self.assertEqual(subscription.status, "Trialling") + + def test_create_subscription_without_trial_with_correct_period(self): + subscription = create_subscription() + self.assertEqual(subscription.trial_period_start, None) + self.assertEqual(subscription.trial_period_end, None) + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + # No invoice is created + self.assertEqual(len(subscription.invoices), 0) + self.assertEqual(subscription.status, "Active") + + def test_create_subscription_trial_with_wrong_dates(self): + subscription = create_subscription( + trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True + ) + self.assertRaises(frappe.ValidationError, subscription.save) + + def test_invoice_is_generated_at_end_of_billing_period(self): + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, "2018-01-01") + self.assertEqual(subscription.current_invoice_end, "2018-01-31") + + subscription.process(posting_date="2018-01-31") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.current_invoice_start, "2018-02-01") + self.assertEqual(subscription.current_invoice_end, "2018-02-28") + self.assertEqual(subscription.status, "Unpaid") + + def test_status_goes_back_to_active_after_invoice_is_paid(self): + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(posting_date="2018-01-01") # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.get_current_invoice() + current_invoice = subscription.get_current_invoice() + + self.assertIsNotNone(current_invoice) + + current_invoice.db_set("outstanding_amount", 0) + current_invoice.db_set("status", "Paid") + subscription.process() + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) + self.assertEqual(len(subscription.invoices), 1) + + def test_subscription_cancel_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + + subscription.process(posting_date="2018-01-31") # generate first invoice + # This should change status to Cancelled since grace period is 0 + # And is backdated subscription so subscription will be cancelled after processing + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_unpaid_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_invoice_days_until_due(self): + _date = add_months(nowdate(), -1) + subscription = create_subscription(start_date=_date, days_until_due=10) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Active") + + def test_subscription_is_past_due_doesnt_change_within_grace_period(self): + settings = frappe.get_single("Subscription Settings") + grace_period = settings.grace_period + settings.grace_period = 1000 + settings.save() + + subscription = create_subscription(start_date=add_days(nowdate(), -1000)) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + # Grace period is 1000 days so status should remain as Past Due Date + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + settings.grace_period = grace_period + settings.save() + + def test_subscription_remains_active_during_invoice_period(self): + subscription = create_subscription() # no changes expected + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected yet still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + def test_subscription_cancellation(self): + subscription = create_subscription() + subscription.cancel_subscription() + + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_cancellation_invoices(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + + self.assertEqual(subscription.status, "Active") + + subscription.cancel_subscription() + # Invoice must have been generated + self.assertEqual(len(subscription.invoices), 1) + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual( + flt( + get_prorata_factor( + subscription.current_invoice_end, + subscription.current_invoice_start, + cint(subscription.generate_invoice_at == "Beginning of the current subscription period"), + ), + 2, + ), + flt(prorate_factor, 2), + ) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + self.assertEqual(subscription.status, "Cancelled") + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_false(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 0 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.grand_total, 900) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process() # generate first invoice + + # Generate an invoice for the cancelled period + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_restart_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.grace_period = 0 + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + + subscription.restart_subscription() + self.assertEqual(subscription.status, "Active") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_unpaid_back_to_active(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(subscription.current_invoice_start) # generate first invoice + # This should change status to Unpaid since grace period is 0 + self.assertEqual(subscription.status, "Unpaid") + + invoice = subscription.get_current_invoice() + invoice.db_set("outstanding_amount", 0) + invoice.db_set("status", "Paid") + + subscription.process() + self.assertEqual(subscription.status, "Active") + + # A new invoice is generated + subscription.process(posting_date=subscription.current_invoice_start) + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_restart_active_subscription(self): + subscription = create_subscription() + self.assertRaises(frappe.ValidationError, subscription.restart_subscription) + + def test_subscription_invoice_discount_percentage(self): + subscription = create_subscription(additional_discount_percentage=10) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.additional_discount_percentage, 10) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_subscription_invoice_discount_amount(self): + subscription = create_subscription(additional_discount_amount=11) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.discount_amount, 11) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_prepaid_subscriptions(self): + # Create a non pre-billed subscription, processing should not create + # invoices. + subscription = create_subscription() + subscription.process() + self.assertEqual(len(subscription.invoices), 0) + + # Change the subscription type to prebilled and process it. + # Prepaid invoice should be generated + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.save() + subscription.process() + + self.assertEqual(len(subscription.invoices), 1) + + def test_prepaid_subscriptions_with_prorate_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription( + generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process() + subscription.cancel_subscription() + + self.assertEqual(len(subscription.invoices), 1) + + current_inv = subscription.get_current_invoice() + self.assertEqual(current_inv.status, "Unpaid") + + prorate_factor = 1 + + self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_with_follow_calendar_months(self): + subscription = frappe.new_doc("Subscription") + subscription.company = "_Test Company" + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.follow_calendar_months = 1 + + # select subscription start date as "2018-01-15" + subscription.start_date = "2018-01-15" + subscription.end_date = "2018-07-15" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) + subscription.save() + + # even though subscription starts at "2018-01-15" and Billing interval is Month and count 3 + # First invoice will end at "2018-03-31" instead of "2018-04-14" + self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") + + def test_subscription_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + party_type="Supplier", + party="_Test Supplier", + generate_invoice_at="Beginning of the current subscription period", + generate_new_invoices_past_due_date=1, + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process(posting_date="2018-01-01") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Now the Subscription is unpaid + # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in + # subscription and the interval between the subscriptions is 3 months + subscription.process(posting_date="2018-04-01") + self.assertEqual(len(subscription.invoices), 2) + + def test_subscription_without_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + + def test_multi_currency_subscription(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], + party="_Test Subscription Customer", + ) + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Check the currency of the created invoice + currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency") + self.assertEqual(currency, "USD") + + def test_subscription_recovery(self): + """Test if Subscription recovers when start/end date run out of sync with created invoices.""" + subscription = create_subscription( + start_date="2021-01-01", + submit_invoice=0, + generate_new_invoices_past_due_date=1, + party="_Test Subscription Customer", + ) + + # create invoices for the first two moths + subscription.process(posting_date="2021-01-31") + + subscription.process(posting_date="2021-02-28") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + # recreate most recent invoice + subscription.process(posting_date="2022-01-31") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + def test_subscription_invoice_generation_before_days(self): + subscription = create_subscription( + start_date="2023-01-01", + generate_invoice_at="Days before the current subscription period", + number_of_days=10, + generate_new_invoices_past_due_date=1, + ) + + subscription.process(posting_date="2022-12-22") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date="2023-01-22") + self.assertEqual(len(subscription.invoices), 2) + + +def make_plans(): + create_plan(plan_name="_Test Plan Name", cost=900) + create_plan(plan_name="_Test Plan Name 2", cost=1999) + create_plan( + plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 + ) + create_plan( + plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 + ) + create_plan( + plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" + ) + + +def create_plan(**kwargs): + if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")): +>>>>>>> 3bdf4f628c (refactor(test): use test fixture in subscription) plan = frappe.new_doc("Subscription Plan") plan.plan_name = "_Test Plan Name" plan.item = "_Test Non Stock Item" From 33becb7b325046d1b17ab6c0362219bcbb94bd53 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 06:35:22 +0530 Subject: [PATCH 17/40] refactor(test): use test fixture in purchase invoice (cherry picked from commit a2e064d214ffb9f012f2144bee14cd467e935241) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 47126d3846f..60892e9fc84 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -5,7 +5,7 @@ import unittest import frappe -from frappe.tests.utils import change_settings +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext @@ -33,7 +33,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): +class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -43,6 +43,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): def tearDownClass(self): unlink_payment_on_cancel_of_invoice(0) + def tearDown(self): + frappe.db.rollback() + def test_purchase_invoice_received_qty(self): """ 1. Test if received qty is validated against accepted + rejected From d78316869b5e13321f5ac3266b6eed19272fa550 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 07:11:11 +0530 Subject: [PATCH 18/40] refactor(test): make use of @change_settings in PI test cases (cherry picked from commit 0207d6e7c996cd6c1b04f2ba171fdf3d6ccfa130) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 60892e9fc84..170a163b45f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -420,6 +420,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -474,6 +475,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ) ) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_invoice_with_advance_and_multi_payment_terms(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -1212,6 +1214,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): unlink_enabled = frappe.db.get_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" @@ -1414,6 +1417,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry From 8d1eac89e3f858b07bc9c27e0948a550c9e16c89 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 08:07:29 +0530 Subject: [PATCH 19/40] refactor(test): make sure TDS Payable is available for testing (cherry picked from commit fbabf4ac2e96c473884c94e59b715d14dee3f960) --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6aae93388d8..ceeed34c821 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2784,6 +2784,13 @@ class TestSalesInvoice(FrappeTestCase): company="_Test Company", ) + tds_payable_account = create_account( + account_name="TDS Payable", + account_type="Tax", + parent_account="Duties and Taxes - _TC", + company="_Test Company", + ) + si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account From 7f903532f396445be30fe57af61ff7bb88c649e5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 10:40:57 +0530 Subject: [PATCH 20/40] chore: resovle conflicts --- .../sales_invoice/test_sales_invoice.py | 27 - .../doctype/subscription/test_subscription.py | 530 +----------------- 2 files changed, 5 insertions(+), 552 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ceeed34c821..272382e8c18 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,12 +6,8 @@ import unittest import frappe from frappe.model.dynamic_links import get_dynamic_link_map -<<<<<<< HEAD from frappe.model.naming import make_autoname -from frappe.tests.utils import change_settings -======= from frappe.tests.utils import FrappeTestCase, change_settings ->>>>>>> c322e5f381 (test: use fixtures for sales and purchase invoice) from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext @@ -3091,10 +3087,6 @@ class TestSalesInvoice(FrappeTestCase): @change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)}) def test_sales_invoice_submission_post_account_freezing_date(self): -<<<<<<< HEAD - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) -======= ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -3103,11 +3095,6 @@ class TestSalesInvoice(FrappeTestCase): si.posting_date = getdate() si.submit() -<<<<<<< HEAD - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) - -======= ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3215,20 +3202,6 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) -<<<<<<< HEAD -<<<<<<< HEAD - acc_settings = frappe.get_single("Accounts Settings") - acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entries = 0 - acc_settings.save() - - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) -======= - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) - -======= ->>>>>>> 8ebe5733ac (refactor(test): fix broken test cases in Sales Invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 4acddfec7e2..89ba0c8055e 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -20,532 +20,8 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") -<<<<<<< HEAD def create_plan(): if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): -======= -class TestSubscription(FrappeTestCase): - def setUp(self): - make_plans() - create_parties() - reset_settings() - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - - def tearDown(self): - frappe.db.rollback() - - def test_create_subscription_with_trial_with_correct_period(self): - subscription = create_subscription( - trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1) - ) - self.assertEqual(subscription.trial_period_start, nowdate()) - self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) - self.assertEqual( - add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) - ) - self.assertEqual( - add_to_date(subscription.current_invoice_start, months=1, days=-1), - get_date_str(subscription.current_invoice_end), - ) - self.assertEqual(subscription.invoices, []) - self.assertEqual(subscription.status, "Trialling") - - def test_create_subscription_without_trial_with_correct_period(self): - subscription = create_subscription() - self.assertEqual(subscription.trial_period_start, None) - self.assertEqual(subscription.trial_period_end, None) - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - # No invoice is created - self.assertEqual(len(subscription.invoices), 0) - self.assertEqual(subscription.status, "Active") - - def test_create_subscription_trial_with_wrong_dates(self): - subscription = create_subscription( - trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True - ) - self.assertRaises(frappe.ValidationError, subscription.save) - - def test_invoice_is_generated_at_end_of_billing_period(self): - subscription = create_subscription(start_date="2018-01-01") - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, "2018-01-01") - self.assertEqual(subscription.current_invoice_end, "2018-01-31") - - subscription.process(posting_date="2018-01-31") - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, "2018-02-01") - self.assertEqual(subscription.current_invoice_end, "2018-02-28") - self.assertEqual(subscription.status, "Unpaid") - - def test_status_goes_back_to_active_after_invoice_is_paid(self): - subscription = create_subscription( - start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process(posting_date="2018-01-01") # generate first invoice - self.assertEqual(len(subscription.invoices), 1) - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - subscription.get_current_invoice() - current_invoice = subscription.get_current_invoice() - - self.assertIsNotNone(current_invoice) - - current_invoice.db_set("outstanding_amount", 0) - current_invoice.db_set("status", "Paid") - subscription.process() - - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) - self.assertEqual(len(subscription.invoices), 1) - - def test_subscription_cancel_after_grace_period(self): - settings = frappe.get_single("Subscription Settings") - settings.cancel_after_grace = 1 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - self.assertEqual(subscription.status, "Active") - - subscription.process(posting_date="2018-01-31") # generate first invoice - # This should change status to Cancelled since grace period is 0 - # And is backdated subscription so subscription will be cancelled after processing - self.assertEqual(subscription.status, "Cancelled") - - def test_subscription_unpaid_after_grace_period(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process(posting_date="2018-01-31") # generate first invoice - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_invoice_days_until_due(self): - _date = add_months(nowdate(), -1) - subscription = create_subscription(start_date=_date, days_until_due=10) - - subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Active") - - def test_subscription_is_past_due_doesnt_change_within_grace_period(self): - settings = frappe.get_single("Subscription Settings") - grace_period = settings.grace_period - settings.grace_period = 1000 - settings.save() - - subscription = create_subscription(start_date=add_days(nowdate(), -1000)) - - subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - # Grace period is 1000 days so status should remain as Past Due Date - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - self.assertEqual(subscription.status, "Past Due Date") - - settings.grace_period = grace_period - settings.save() - - def test_subscription_remains_active_during_invoice_period(self): - subscription = create_subscription() # no changes expected - - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - subscription.process() # no changes expected still - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - subscription.process() # no changes expected yet still - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - def test_subscription_cancellation(self): - subscription = create_subscription() - subscription.cancel_subscription() - - self.assertEqual(subscription.status, "Cancelled") - - def test_subscription_cancellation_invoices(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription() - - self.assertEqual(subscription.status, "Active") - - subscription.cancel_subscription() - # Invoice must have been generated - self.assertEqual(len(subscription.invoices), 1) - - invoice = subscription.get_current_invoice() - diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) - prorate_factor = flt(diff / plan_days) - - self.assertEqual( - flt( - get_prorata_factor( - subscription.current_invoice_end, - subscription.current_invoice_start, - cint(subscription.generate_invoice_at == "Beginning of the current subscription period"), - ), - 2, - ), - flt(prorate_factor, 2), - ) - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, "Cancelled") - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_invoices_with_prorata_false(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 0 - settings.save() - - subscription = create_subscription() - subscription.cancel_subscription() - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.grand_total, 900) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_invoices_with_prorata_true(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription() - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) - prorate_factor = flt(diff / plan_days) - - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_and_process(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 1 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process() # generate first invoice - - # Generate an invoice for the cancelled period - subscription.cancel_subscription() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_restart_and_process(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.grace_period = 0 - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process(posting_date="2018-01-31") # generate first invoice - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - subscription.cancel_subscription() - self.assertEqual(subscription.status, "Cancelled") - - subscription.restart_subscription() - self.assertEqual(subscription.status, "Active") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Unpaid") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Unpaid") - self.assertEqual(len(subscription.invoices), 1) - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_unpaid_back_to_active(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription( - start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process(subscription.current_invoice_start) # generate first invoice - # This should change status to Unpaid since grace period is 0 - self.assertEqual(subscription.status, "Unpaid") - - invoice = subscription.get_current_invoice() - invoice.db_set("outstanding_amount", 0) - invoice.db_set("status", "Paid") - - subscription.process() - self.assertEqual(subscription.status, "Active") - - # A new invoice is generated - subscription.process(posting_date=subscription.current_invoice_start) - self.assertEqual(subscription.status, "Unpaid") - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_restart_active_subscription(self): - subscription = create_subscription() - self.assertRaises(frappe.ValidationError, subscription.restart_subscription) - - def test_subscription_invoice_discount_percentage(self): - subscription = create_subscription(additional_discount_percentage=10) - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.additional_discount_percentage, 10) - self.assertEqual(invoice.apply_discount_on, "Grand Total") - - def test_subscription_invoice_discount_amount(self): - subscription = create_subscription(additional_discount_amount=11) - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.discount_amount, 11) - self.assertEqual(invoice.apply_discount_on, "Grand Total") - - def test_prepaid_subscriptions(self): - # Create a non pre-billed subscription, processing should not create - # invoices. - subscription = create_subscription() - subscription.process() - self.assertEqual(len(subscription.invoices), 0) - - # Change the subscription type to prebilled and process it. - # Prepaid invoice should be generated - subscription.generate_invoice_at = "Beginning of the current subscription period" - subscription.save() - subscription.process() - - self.assertEqual(len(subscription.invoices), 1) - - def test_prepaid_subscriptions_with_prorate_true(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription( - generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process() - subscription.cancel_subscription() - - self.assertEqual(len(subscription.invoices), 1) - - current_inv = subscription.get_current_invoice() - self.assertEqual(current_inv.status, "Unpaid") - - prorate_factor = 1 - - self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_with_follow_calendar_months(self): - subscription = frappe.new_doc("Subscription") - subscription.company = "_Test Company" - subscription.party_type = "Supplier" - subscription.party = "_Test Supplier" - subscription.generate_invoice_at = "Beginning of the current subscription period" - subscription.follow_calendar_months = 1 - - # select subscription start date as "2018-01-15" - subscription.start_date = "2018-01-15" - subscription.end_date = "2018-07-15" - subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) - subscription.save() - - # even though subscription starts at "2018-01-15" and Billing interval is Month and count 3 - # First invoice will end at "2018-03-31" instead of "2018-04-14" - self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") - - def test_subscription_generate_invoice_past_due(self): - subscription = create_subscription( - start_date="2018-01-01", - party_type="Supplier", - party="_Test Supplier", - generate_invoice_at="Beginning of the current subscription period", - generate_new_invoices_past_due_date=1, - plans=[{"plan": "_Test Plan Name 4", "qty": 1}], - ) - - # Process subscription and create first invoice - # Subscription status will be unpaid since due date has already passed - subscription.process(posting_date="2018-01-01") - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - # Now the Subscription is unpaid - # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in - # subscription and the interval between the subscriptions is 3 months - subscription.process(posting_date="2018-04-01") - self.assertEqual(len(subscription.invoices), 2) - - def test_subscription_without_generate_invoice_past_due(self): - subscription = create_subscription( - start_date="2018-01-01", - generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Name 4", "qty": 1}], - ) - - # Process subscription and create first invoice - # Subscription status will be unpaid since due date has already passed - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - - def test_multi_currency_subscription(self): - subscription = create_subscription( - start_date="2018-01-01", - generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], - party="_Test Subscription Customer", - ) - - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - # Check the currency of the created invoice - currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency") - self.assertEqual(currency, "USD") - - def test_subscription_recovery(self): - """Test if Subscription recovers when start/end date run out of sync with created invoices.""" - subscription = create_subscription( - start_date="2021-01-01", - submit_invoice=0, - generate_new_invoices_past_due_date=1, - party="_Test Subscription Customer", - ) - - # create invoices for the first two moths - subscription.process(posting_date="2021-01-31") - - subscription.process(posting_date="2021-02-28") - - self.assertEqual(len(subscription.invoices), 2) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), - getdate("2021-01-01"), - ) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), - getdate("2021-02-01"), - ) - - # recreate most recent invoice - subscription.process(posting_date="2022-01-31") - - self.assertEqual(len(subscription.invoices), 2) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), - getdate("2021-01-01"), - ) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), - getdate("2021-02-01"), - ) - - def test_subscription_invoice_generation_before_days(self): - subscription = create_subscription( - start_date="2023-01-01", - generate_invoice_at="Days before the current subscription period", - number_of_days=10, - generate_new_invoices_past_due_date=1, - ) - - subscription.process(posting_date="2022-12-22") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process(posting_date="2023-01-22") - self.assertEqual(len(subscription.invoices), 2) - - -def make_plans(): - create_plan(plan_name="_Test Plan Name", cost=900) - create_plan(plan_name="_Test Plan Name 2", cost=1999) - create_plan( - plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 - ) - create_plan( - plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 - ) - create_plan( - plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" - ) - - -def create_plan(**kwargs): - if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")): ->>>>>>> 3bdf4f628c (refactor(test): use test fixture in subscription) plan = frappe.new_doc("Subscription Plan") plan.plan_name = "_Test Plan Name" plan.item = "_Test Non Stock Item" @@ -615,10 +91,14 @@ def create_parties(): customer.insert() -class TestSubscription(unittest.TestCase): +class TestSubscription(FrappeTestCase): def setUp(self): create_plan() create_parties() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() def test_create_subscription_with_trial_with_correct_period(self): subscription = frappe.new_doc("Subscription") From 78e22af3caf6b07be1b80a03f3cd78ee874e1925 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 11:19:16 +0530 Subject: [PATCH 21/40] fix: inflated total amt in TDS report using back calculation --- .../report/tds_payable_monthly/tds_payable_monthly.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 91ad3d6873a..f2ec31c70e1 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -68,7 +68,11 @@ def get_result( tax_amount += entry.credit - entry.debit if net_total_map.get(name): - total_amount, grand_total, base_total = net_total_map.get(name) + if voucher_type == "Journal Entry": + # back calcalute total amount from rate and tax_amount + total_amount = grand_total = base_total = tax_amount / (rate / 100) + else: + total_amount, grand_total, base_total = net_total_map.get(name) else: total_amount += entry.credit From d1f6d62d72c9543996ef638d3d3005b36fc054c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 12:19:34 +0530 Subject: [PATCH 22/40] chore: fix flaky test case --- .../test_bank_reconciliation_statement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py index d7c871608ec..b1be53ba73f 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py @@ -23,6 +23,7 @@ class TestBankReconciliationStatement(FrappeTestCase): "Payment Entry", ]: frappe.db.delete(dt) + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) def test_loan_entries_in_bank_reco_statement(self): create_loan_accounts() From 1b94510f08cb50dd786eddc6a20f1ef5f321beb0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:23:13 +0530 Subject: [PATCH 23/40] fix: consider received qty while creating SO -> MR (backport #37414) (#37514) fix: consider received qty while creating SO -> MR (cherry picked from commit b2cee396ac9edea5ba920382bfc27f3736600775) Co-authored-by: s-aga-r --- .../doctype/sales_order/sales_order.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 485ac60e744..25ea9189553 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -542,29 +542,37 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): - return frappe._dict( - frappe.db.sql( - """ - select sales_order_item, sum(qty) - from `tabMaterial Request Item` - where docstatus = 1 - and sales_order = %s - group by sales_order_item - """, - sales_order, - ) - ) + result = {} + for d in frappe.db.get_all( + "Material Request Item", + filters={"docstatus": 1, "sales_order": sales_order}, + fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"], + group_by="sales_order_item", + ): + result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) + + return result @frappe.whitelist() def make_material_request(source_name, target_doc=None): requested_item_qty = get_requested_item_qty(source_name) + def get_remaining_qty(so_item): + return flt( + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(so_item.get("delivered_qty")) + - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), + 0, + ) + ) + def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field - qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) + target.qty = get_remaining_qty(source) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -597,8 +605,8 @@ def make_material_request(source_name, target_doc=None): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), + "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + and get_remaining_qty(item) > 0, "postprocess": update_item, }, }, From c32258e4b690758b4a2e667edd85caa99083ab62 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:24:10 +0530 Subject: [PATCH 24/40] fix: GL Entries not getting created for PR Return (backport #37513) (#37516) * fix: GL Entries not getting created for PR Return (cherry picked from commit 46add06a29f8a0a5990dfd3aeda39f01413071bb) * test: add test case for PR return with zero rate (cherry picked from commit 253d4782c63963df78216ce51d9f9f9a80791531) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4a651cd0d18..da534b245c6 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -341,7 +341,7 @@ class PurchaseReceipt(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): - if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): + if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 463353e2549..a93d5b1bbbe 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2147,6 +2147,62 @@ class TestPurchaseReceipt(FrappeTestCase): # Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2)) + def test_purchase_return_with_zero_rate(self): + company = "_Test Company with perpetual inventory" + + # Step - 1: Create Item + item, warehouse = ( + make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name, + "Stores - TCP1", + ) + + # Step - 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se = make_stock_entry( + purpose="Material Receipt", + item_code=item, + qty=100, + basic_rate=100, + to_warehouse=warehouse, + company=company, + ) + + # Step - 3: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=5, + rate=0, + warehouse=warehouse, + company=company, + ) + + # Step - 4: Create Purchase Return + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pr_return = make_return_doc("Purchase Receipt", pr.name) + pr_return.save() + pr_return.submit() + + sl_entries = get_sl_entries(pr_return.doctype, pr_return.name) + gl_entries = get_gl_entries(pr_return.doctype, pr_return.name) + + # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate + average_rate = ( + (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate) + ) / (se.items[0].qty + pr.items[0].qty) + expected_stock_value_difference = pr_return.items[0].qty * average_rate + self.assertEqual( + flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2) + ) + + # Test - 2: GL Entries should be created for Stock Value Difference + self.assertEqual(len(gl_entries), 2) + + # Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries. + for entry in gl_entries: + self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 44f7de0f31f4464f2823038356bb52f479fa3d5b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Oct 2023 14:37:16 +0530 Subject: [PATCH 25/40] fix: Incorrect vat amount in KSA VAT report --- erpnext/regional/report/ksa_vat/ksa_vat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index 3571f962667..8c1cf3c80fe 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -177,7 +177,8 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): "parent": invoice.name, "item_tax_template": vat_setting.item_tax_template, }, - fields=["item_code", "base_net_amount"], + fields=["item_code", "sum(base_net_amount) as base_net_amount"], + group_by="item_code, item_tax_template", ) for item in invoice_items: From 001c230688b6cd757c7f0bd39afc2c1543c21080 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:48:55 +0530 Subject: [PATCH 26/40] perf: index `dn_detail` in `Delivery Note Item` (backport #37528) (#37530) * perf: index `dn_detail` in `Delivery Note Item` (cherry picked from commit 5b4528e6146aaeb8f86f9fde3f272635d005eeec) # Conflicts: # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../stock/doctype/delivery_note_item/delivery_note_item.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index edfb269da9a..237088f64a7 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -741,7 +741,8 @@ "label": "Against Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "stock_qty_sec_break", @@ -868,7 +869,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-25 11:58:28.101919", + "modified": "2023-10-16 16:18:18.013379", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From 76ef61c24fab6190790034da0e5e8ea1d3d2e242 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 16:53:43 +0530 Subject: [PATCH 27/40] fix: keep customer/supplier website role by default (cherry picked from commit d2096cfdb752bff03f8d3a00262d86c9eeb76c37) --- erpnext/setup/install.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1e047d1e4dd..71b1ca7c058 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -33,6 +33,7 @@ def after_install(): add_app_name() setup_log_settings() hide_workspaces() + update_roles() frappe.db.commit() @@ -214,6 +215,12 @@ def hide_workspaces(): frappe.db.set_value("Workspace", ws, "public", 0) +def update_roles(): + website_user_roles = ("Customer", "Supplier") + for role in website_user_roles: + frappe.db.set_value("Role", role, "desk_access", 0) + + def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): role_profile = frappe.new_doc("Role Profile") From 71cb7d37ee9202f4a4e3a4e7f1b55105b4a8a26d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Oct 2023 09:53:41 +0530 Subject: [PATCH 28/40] refactor: checkbox to toggle exchange rate inheritence in PO->PI (cherry picked from commit 08315522bbc198fce1168a5e8522684cad750276) --- .../doctype/purchase_invoice/purchase_invoice.json | 10 +++++++++- .../doctype/buying_settings/buying_settings.json | 10 +++++++++- erpnext/controllers/accounts_controller.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 1f3b17ee147..02a5114f345 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -36,6 +36,7 @@ "currency_and_price_list", "currency", "conversion_rate", + "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", @@ -1578,13 +1579,20 @@ "label": "Repost Required", "options": "Account", "read_only": 1 + }, + { + "default": "0", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-10-01 21:01:47.282533", + "modified": "2023-10-16 16:24:51.886231", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 8c73e56a99e..71cb01b188f 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -24,6 +24,7 @@ "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", "show_pay_button", + "use_transaction_date_exchange_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -164,6 +165,13 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" + }, + { + "default": "0", + "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-03-02 17:02:14.404622", + "modified": "2023-10-16 16:22:03.201078", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ab20f82ea25..7ae1b0c1ad2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -583,6 +583,17 @@ class AccountsController(TransactionBase): self.currency, self.company_currency, transaction_date, args ) + if ( + self.currency + and buying_or_selling == "Buying" + and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate") + and self.doctype == "Purchase Invoice" + ): + self.use_transaction_date_exchange_rate = True + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency, transaction_date, args + ) + def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos From 50258502585d30791bd6716b4d2e2935721047f9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 17 Oct 2023 12:20:23 +0530 Subject: [PATCH 29/40] fix: same Serial No get mapped while creating SO -> DN (#37527) * fix: same Serial No get mapped while creating SO -> DN * test: add test case for DN with repetitive serial item --- erpnext/controllers/accounts_controller.py | 7 +++++ .../delivery_note/test_delivery_note.py | 29 ++++++++++++++++++- erpnext/stock/get_item_details.py | 4 +++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7ae1b0c1ad2..c17866162b6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -617,6 +617,7 @@ class AccountsController(TransactionBase): self.pricing_rules = [] + selected_serial_nos_map = {} for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() @@ -628,6 +629,7 @@ class AccountsController(TransactionBase): args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 ) + args["ignore_serial_nos"] = selected_serial_nos_map.get(item.get("item_code")) if not args.get("transaction_date"): args["transaction_date"] = args.get("posting_date") @@ -684,6 +686,11 @@ class AccountsController(TransactionBase): if ret.get("pricing_rules"): self.apply_pricing_rule_on_items(item, ret) self.set_pricing_rule_details(item, ret) + + if ret.get("serial_no"): + selected_serial_nos_map.setdefault(item.get("item_code"), []).extend( + ret.get("serial_no").split("\n") + ) else: # Transactions line item without item code diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8ea87f00c5b..c6f3197a668 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -5,7 +5,7 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1284,6 +1284,33 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(return_dn.packed_items[0].batch_no, dn.packed_items[0].batch_no) self.assertEqual(return_dn.packed_items[1].serial_no, dn.packed_items[1].serial_no) + @change_settings("Stock Settings", {"automatically_set_serial_nos_based_on_fifo": 1}) + def test_delivery_note_for_repetitive_serial_item(self): + # Step - 1: Create Serial Item + item, warehouse = ( + make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.###"} + ).name, + "_Test Warehouse - _TC", + ) + + # Step - 2: Inward Stock + make_stock_entry(item_code=item, target=warehouse, qty=5) + + # Step - 3: Create Delivery Note with repetitive Serial Item + dn = create_delivery_note(item_code=item, warehouse=warehouse, qty=2, do_not_save=True) + dn.append("items", dn.items[0].as_dict()) + dn.items[1].qty = 3 + dn.save() + dn.submit() + + # Test - 1: Serial Nos should be different for each line item + serial_nos = [] + for item in dn.items: + for serial_no in item.serial_no.split("\n"): + self.assertNotIn(serial_no, serial_nos) + serial_nos.append(serial_no) + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 13f484b1c85..92c945b254b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -160,6 +160,7 @@ def update_stock(args, out): and out.warehouse and out.stock_qty > 0 ): + out["ignore_serial_nos"] = args.get("ignore_serial_nos") if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -1140,6 +1141,8 @@ def get_serial_nos_by_fifo(args, sales_order=None): query = query.where(sn.sales_order == sales_order) if args.batch_no: query = query.where(sn.batch_no == args.batch_no) + if args.ignore_serial_nos: + query = query.where(sn.name.notin(args.ignore_serial_nos)) serial_nos = query.run(as_list=True) serial_nos = [s[0] for s in serial_nos] @@ -1450,6 +1453,7 @@ def get_serial_no(args, serial_nos=None, sales_order=None): "item_code": args.get("item_code"), "warehouse": args.get("warehouse"), "stock_qty": args.get("stock_qty"), + "ignore_serial_nos": args.get("ignore_serial_nos"), } ) args = process_args(args) From e23710bf005c83abd84a1bc5cdaefcd17e3c7427 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 15:03:16 +0530 Subject: [PATCH 30/40] fix(test): project test case (backport #37541) (#37543) fix(test): project test case (cherry picked from commit fd6aee15e6bf3b7ea18487ab1b24b0a77526ac85) Co-authored-by: s-aga-r --- erpnext/projects/doctype/task_depends_on/task_depends_on.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index 5102986f00d..3300b7eb905 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -24,6 +24,7 @@ }, { "fetch_from": "task.subject", + "fetch_if_empty": 1, "fieldname": "subject", "fieldtype": "Text", "in_list_view": 1, @@ -31,7 +32,6 @@ "read_only": 1 }, { - "fetch_from": "task.project", "fieldname": "project", "fieldtype": "Text", "label": "Project", @@ -40,7 +40,7 @@ ], "istable": 1, "links": [], - "modified": "2023-10-09 11:34:14.335853", + "modified": "2023-10-17 12:45:21.536165", "modified_by": "Administrator", "module": "Projects", "name": "Task Depends On", From bfa93cd3f6ac85b4d203e31cea0414c6765b5378 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 18:19:47 +0530 Subject: [PATCH 31/40] chore: Add accounting dimensions to Sales Order Item table (cherry picked from commit e31db1891237aa22c19d71929f69d9db5596ae3c) # Conflicts: # erpnext/patches.txt --- .../accounting_dimension.py | 27 +++++++++++++++++++ erpnext/hooks.py | 1 + erpnext/patches.txt | 7 +++++ ...counting_dimensions_in_sales_order_item.py | 7 +++++ .../sales_order_item/sales_order_item.json | 11 ++++++-- 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index cfe5e6e8009..8afd313322e 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -301,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False): default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension return dimension_filters, default_dimensions_map + + +def create_accounting_dimensions_for_doctype(doctype): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b2a76f2038c..6d5dddd9e97 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -517,6 +517,7 @@ accounting_dimension_doctypes = [ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", + "Sales Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 37e70967388..ec4c405feee 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,5 +341,12 @@ execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults +<<<<<<< HEAD +======= +erpnext.patches.v14_0.update_invoicing_period_in_subscription +execute:frappe.delete_doc("Page", "welcome-to-erpnext") +erpnext.patches.v15_0.delete_payment_gateway_doctypes +erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item +>>>>>>> e31db18912 (chore: Add accounting dimensions to Sales Order Item table) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py new file mode 100644 index 00000000000..8f77c35b129 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Sales Order Item") diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index a3e9977244c..e9714113095 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -66,6 +66,7 @@ "total_weight", "column_break_21", "weight_uom", + "accounting_dimensions_section", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -868,12 +869,18 @@ "label": "Production Plan Qty", "no_copy": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-28 14:56:42.031636", + "modified": "2023-10-17 18:18:26.475259", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -884,4 +891,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From 7db69883649671024dcc81c669d0bcce0676a50a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 18 Oct 2023 09:00:49 +0530 Subject: [PATCH 32/40] chore: resolve conflicts --- erpnext/patches.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ec4c405feee..abdd09383bb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,12 +341,6 @@ execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults -<<<<<<< HEAD -======= -erpnext.patches.v14_0.update_invoicing_period_in_subscription -execute:frappe.delete_doc("Page", "welcome-to-erpnext") -erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item ->>>>>>> e31db18912 (chore: Add accounting dimensions to Sales Order Item table) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 3499089323908f9b8748c72d43bf4c57de47ab0d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 16:50:20 +0530 Subject: [PATCH 33/40] refactor: use account in key while grouping voucher in ar/ap report (cherry picked from commit 601ab4567ea7062b78448235fc2fc62b15dce6a6) --- .../report/accounts_receivable/accounts_receivable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e3b671f3973..b9c7a0bfb87 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -116,7 +116,7 @@ class ReceivablePayableReport(object): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: # get the balance object for voucher_type - key = (ple.voucher_type, ple.voucher_no, ple.party) + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, @@ -183,7 +183,7 @@ class ReceivablePayableReport(object): ): return - key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) # If payment is made against credit note # and credit note is made against a Sales Invoice @@ -192,13 +192,13 @@ class ReceivablePayableReport(object): if ple.against_voucher_no in self.return_entries: return_against = self.return_entries.get(ple.against_voucher_no) if return_against: - key = (ple.against_voucher_type, return_against, ple.party) + key = (ple.account, ple.against_voucher_type, return_against, ple.party) row = self.voucher_balance.get(key) if not row: # no invoice, this is an invoice / stand-alone payment / credit note - row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row.party_type = ple.party_type return row From 760eab961ddbf3568450ed5a3847de3582f08f9d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 20:37:48 +0530 Subject: [PATCH 34/40] test: report output if party is missing (cherry picked from commit 244cec64b2641c50bd6102e6dba65a481d24da0d) --- .../test_accounts_receivable.py | 115 ++++++++++++++---- 1 file changed, 92 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 4307689158f..cbeb6d3106d 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, today @@ -23,29 +24,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def create_usd_account(self): - name = "Debtors USD" - exists = frappe.db.get_list( - "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} - ) - if exists: - self.debtors_usd = exists[0].name - else: - debtors = frappe.get_doc( - "Account", - frappe.db.get_list( - "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} - )[0].name, - ) - - debtors_usd = frappe.new_doc("Account") - debtors_usd.company = debtors.company - debtors_usd.account_name = "Debtors USD" - debtors_usd.account_currency = "USD" - debtors_usd.parent_account = debtors.parent_account - debtors_usd.account_type = debtors.account_type - self.debtors_usd = debtors_usd.save().name - def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -643,3 +621,94 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(report[1]), 2) output_for = set([x.party for x in report[1]]) self.assertEqual(output_for, expected_output) + + def test_report_output_if_party_is_missing(self): + acc_name = "Additional Debtors" + if not frappe.db.get_value( + "Account", filters={"account_name": acc_name, "company": self.company} + ): + additional_receivable_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": acc_name, + "parent_account": "Accounts Receivable - " + self.company_abbr, + "company": self.company, + "account_type": "Receivable", + } + ).save() + self.debtors2 = additional_receivable_acc.name + + je = frappe.new_doc("Journal Entry") + je.company = self.company + je.posting_date = today() + je.append( + "accounts", + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 150, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.debtors2, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 200, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.cash, + "debit_in_account_currency": 0, + "credit_in_account_currency": 350, + "cost_center": self.cost_center, + }, + ) + je.save().submit() + + # manually remove party from Payment Ledger + ple = qb.DocType("Payment Ledger Entry") + qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report_ouput = execute(filters)[1] + expected_data = [ + [self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0], + [self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0], + ] + self.assertEqual(len(report_ouput), 2) + # fetch only required fields + report_output = [ + [ + x.party_account, + x.voucher_type, + x.voucher_no, + "Customer", + self.customer, + x.invoiced, + x.paid, + x.credit_note, + x.outstanding, + ] + for x in report_ouput + ] + # use account name to sort + # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] + report_output = sorted(report_output, key=lambda x: x[0]) + self.assertEqual(expected_data, report_output) From ac7d6d6d59c96a2e05ba8f68ce47b3576ba4aa48 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:00:29 +0000 Subject: [PATCH 35/40] fix: billed_qty to show a sum of all invoiced qty from the purchase order item. (backport #37539) (#37558) fix: billed_qty to show a sum of all invoiced qty from the purchase order item. (cherry picked from commit 8a72f4f58aee90e7155105b7275113555e788401) Co-authored-by: HarryPaulo --- .../report/purchase_order_analysis/purchase_order_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index e10c0e2fccf..b6e46302ffe 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -6,7 +6,7 @@ import copy import frappe from frappe import _ -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import date_diff, flt, getdate @@ -57,7 +57,7 @@ def get_data(filters): po_item.qty, po_item.received_qty, (po_item.qty - po_item.received_qty).as_("pending_qty"), - IfNull(pi_item.qty, 0).as_("billed_qty"), + Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), po_item.base_amount.as_("amount"), (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), From e8d082560add16592121232bb1814cba4f2ce0fb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:19:27 +0530 Subject: [PATCH 36/40] refactor: move `unreconcile` btn inside a drop down (cherry picked from commit f2b0ac6868387611d9d6d00274fea6655aceb9c0) --- erpnext/public/js/utils/unreconcile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index acc77a64b01..bbdd51d6e54 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -19,7 +19,7 @@ erpnext.accounts.unreconcile_payments = { if (r.message) { frm.add_custom_button(__("Un-Reconcile"), function() { erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); - }); + }, __('Actions')); } } }); From 54f672e1444202595f17c4783b3138eb156ffde1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:21:50 +0530 Subject: [PATCH 37/40] refactor: add `unreconcile` btn to purchase invoice (cherry picked from commit 94ce43b0d5d131e31e6fc01ac06cfa748d73caed) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ee5a50af058..04b00e09a44 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -181,6 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } unblock_invoice() { From 022f85dd085c530955207e77d533f6d136f28156 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:21:46 +0530 Subject: [PATCH 38/40] fix: e-commerce permissions for address (#37554) * fix: E-commerce permissions (cherry picked from commit f4d74990fe1cc2abda56359ce8d09644526c62a6) # Conflicts: # erpnext/controllers/selling_controller.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- erpnext/accounts/party.py | 32 ++++++++++++++++------- erpnext/controllers/selling_controller.py | 6 +++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0c51e727c92..66b56684fae 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -6,11 +6,7 @@ from typing import Optional import frappe from frappe import _, msgprint, scrub -from frappe.contacts.doctype.address.address import ( - get_address_display, - get_company_address, - get_default_address, -) +from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values @@ -133,6 +129,7 @@ def _get_party_details( party_address, company_address, shipping_address, + ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) @@ -193,6 +190,8 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + *, + ignore_permissions=False ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -205,13 +204,17 @@ def set_address_details( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) ) # address display - party_details.address_display = get_address_display(party_details[billing_address_field]) + party_details.address_display = render_address( + party_details[billing_address_field], check_permissions=not ignore_permissions + ) # shipping address if party_type in ["Customer", "Lead"]: party_details.shipping_address_name = shipping_address or get_party_shipping_address( party_type, party.name ) - party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) + party_details.shipping_address = render_address( + party_details["shipping_address_name"], check_permissions=not ignore_permissions + ) if doctype: party_details.update( get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) @@ -229,7 +232,7 @@ def set_address_details( if shipping_address: party_details.update( shipping_address=shipping_address, - shipping_address_display=get_address_display(shipping_address), + shipping_address_display=render_address(shipping_address), **get_fetch_values(doctype, "shipping_address", shipping_address) ) @@ -238,7 +241,8 @@ def set_address_details( party_details.update( billing_address=party_details.company_address, billing_address_display=( - party_details.company_address_display or get_address_display(party_details.company_address) + party_details.company_address_display + or render_address(party_details.company_address, check_permissions=False) ), **get_fetch_values(doctype, "billing_address", party_details.company_address) ) @@ -957,3 +961,13 @@ def add_party_account(party_type, party, company, account): doc.append("accounts", accounts) doc.save() + + +def render_address(address, check_permissions=True): + try: + from frappe.contacts.doctype.address.address import render_address as _render + except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render + + return frappe.call(_render, address, check_permissions=check_permissions) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5daaba8e892..f005a7f7a39 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,9 +4,9 @@ import frappe from frappe import _, bold, throw -from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime +from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController @@ -591,7 +591,9 @@ class SellingController(StockController): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def validate_for_duplicate_items(self): check_list, chk_dupl_itm = [], [] From 95abd7908f7548e14401390f3c74fc8679eaf8c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:38:24 +0530 Subject: [PATCH 39/40] fix: payment entry count on supplier dashboard (backport #37571) (#37575) fix: payment entry count on supplier dashboard (#37571) (cherry picked from commit 10311ff114b7513b47df9be993fa568d6e586f1d) Co-authored-by: rohitwaghchaure --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 11bb06e0caa..3bd306e6591 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -8,7 +8,7 @@ def get_data(): "This is based on transactions against this Supplier. See timeline below for details" ), "fieldname": "supplier", - "non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"}, + "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, "transactions": [ {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, From e1504efd406398f5688f705c863fc1e9bc49dae5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:50:55 +0530 Subject: [PATCH 40/40] fix: Issues related to RFQ and Supplier Quotation on Portal (backport #37565) (#37577) * fix: Issues related to RFQ and Supplier Quotation on Portal (#37565) fix: RFQ and Supplier Quotation for Portal (cherry picked from commit 2851a41310a050afee753c8ac396eb808d0d123c) * chore: removed backport changes --------- Co-authored-by: rohitwaghchaure --- erpnext/accounts/party.py | 30 +++++++++++++++++-- .../includes/order/order_macros.html | 2 +- erpnext/templates/includes/rfq.js | 4 +-- .../templates/includes/rfq/rfq_macros.html | 24 +++++++++------ erpnext/templates/pages/order.html | 4 +-- erpnext/templates/pages/rfq.html | 4 +-- 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 66b56684fae..150b56742e4 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -7,7 +7,6 @@ from typing import Optional import frappe from frappe import _, msgprint, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address -from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import Abs, Date, Sum @@ -294,7 +293,34 @@ def set_contact_details(party_details, party, party_type): } ) else: - party_details.update(get_contact_details(party_details.contact_person)) + fields = [ + "name as contact_person", + "salutation", + "first_name", + "last_name", + "email_id as contact_email", + "mobile_no as contact_mobile", + "phone as contact_phone", + "designation as contact_designation", + "department as contact_department", + ] + + contact_details = frappe.db.get_value( + "Contact", party_details.contact_person, fields, as_dict=True + ) + + contact_details.contact_display = " ".join( + filter( + None, + [ + contact_details.get("salutation"), + contact_details.get("first_name"), + contact_details.get("last_name"), + ], + ) + ) + + party_details.update(contact_details) def set_other_values(party_details, party, party_type): diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html index d95b28961c6..8799a3b1eab 100644 --- a/erpnext/templates/includes/order/order_macros.html +++ b/erpnext/templates/includes/order/order_macros.html @@ -7,7 +7,7 @@ {% if d.thumbnail or d.image %} {{ product_image(d.thumbnail or d.image, no_border=True) }} {% else %} -
+
{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
{% endif %} diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index 37beb5a584b..e78776fd29f 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -72,7 +72,7 @@ rfq = class rfq { } submit_rfq(){ - $('.btn-sm').click(function(){ + $('.btn-sm').click(function() { frappe.freeze(); frappe.call({ type: "POST", @@ -81,7 +81,7 @@ rfq = class rfq { doc: doc }, btn: this, - callback: function(r){ + callback: function(r) { frappe.unfreeze(); if(r.message){ $('.btn-sm').hide() diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html index 88724c30de6..78ec6ff5f8b 100644 --- a/erpnext/templates/includes/rfq/rfq_macros.html +++ b/erpnext/templates/includes/rfq/rfq_macros.html @@ -1,19 +1,25 @@ {% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %} {% macro item_name_and_description(d, doc) %} -
-
- {{ product_image(d.image) }} -
-
- {{ d.item_code }} -

{{ d.description }}

+
+
+ {% if d.image %} + {{ product_image(d.image) }} + {% else %} +
+ {{ frappe.utils.get_abbr(d.item_name)}} +
+ {% endif %} +
+
+ {{ d.item_code }} +

{{ d.description }}

{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}

{% if supplier_part_no %} {{_("Supplier Part No") + ": "+ supplier_part_no}} {% endif %}

-
-
+
+
{% endmacro %} diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index bc34ad5ac50..d9cb75ac5ac 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -165,7 +165,6 @@
{% endif %} - {% if attachments %}
@@ -193,6 +192,7 @@ {% endif %} {% endblock %} + {% block script %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html index 6516482c230..d371bf2161d 100644 --- a/erpnext/templates/pages/rfq.html +++ b/erpnext/templates/pages/rfq.html @@ -1,7 +1,7 @@ {% extends "templates/web.html" %} {% block header %} -

{{ doc.name }}

+

{{ doc.name }}

{% endblock %} {% block script %} @@ -16,7 +16,7 @@ {% if doc.items %} + {{ _("Make Quotation") }} {% endif %} {% endblock %}