diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index db46c5621b2..722c1252ed9 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -12,7 +12,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: '3.10' - name: 'Clone repo' uses: actions/checkout@v2 diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 0da44a464e7..3920d4cf096 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -49,7 +49,6 @@
{% endif %} - {{ _("Against") }}: {{ row.against }}
{{ _("Remarks") }}: {{ row.remarks }} {% if row.bill_no %}
{{ _("Supplier Invoice No") }}: {{ row.bill_no }} diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 79a67b83631..6035e86d067 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1117,6 +1117,46 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") + def test_bin_details_of_packed_item(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + si = create_sales_invoice( + item_code="_Test Product Bundle Item New", + update_stock=1, + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + si.transaction_date = nowdate() + si.save() + + packed_item = si.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_pos_si_without_payment(self): make_pos_profile() diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 1eb257ac853..7d2db26c25f 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object): ret += [{}] # add total row - if ret is not []: - if self.filters.type == "Revenue": - total_row = frappe._dict({"name": "Total Deferred Income"}) - elif self.filters.type == "Expense": - total_row = frappe._dict({"name": "Total Deferred Expense"}) + if self.filters.type == "Revenue": + total_row = frappe._dict({"name": "Total Deferred Income"}) + elif self.filters.type == "Expense": + total_row = frappe._dict({"name": "Total Deferred Expense"}) - for idx, period in enumerate(self.period_list, 0): - total_row[period.key] = self.period_total[idx].total - ret.append(total_row) + for idx, period in enumerate(self.period_list, 0): + total_row[period.key] = self.period_total[idx].total + ret.append(total_row) return ret diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 378fa3791c1..1aad36ca909 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -25,8 +25,8 @@ {%= __("Date") %} - {%= __("Ref") %} - {%= __("Party") %} + {%= __("Reference") %} + {%= __("Remarks") %} {%= __("Debit") %} {%= __("Credit") %} {%= __("Balance (Dr - Cr)") %} @@ -45,7 +45,6 @@
{% } %} - {{ __("Against") }}: {%= data[i].against %}
{%= __("Remarks") %}: {%= data[i].remarks %} {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 590370e808c..2e6df8525cc 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1239,6 +1239,11 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def test_variant_item_po(self): + po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1) + + self.assertRaises(frappe.ValidationError, po.save) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -1342,8 +1347,8 @@ def create_purchase_order(**args): }, ) - po.set_missing_values() if not args.do_not_save: + po.set_missing_values() po.insert() if not args.do_not_submit: if po.is_subcontracted == "Yes": diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 57f8a3e1513..c52a2dfa95b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -25,7 +25,7 @@ class SellingController(StockController): def onload(self): super(SellingController, self).onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): - for item in self.get("items"): + for item in self.get("items") + (self.get("packed_items") or []): item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 62e90ae747d..23dad95fa0d 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -58,7 +58,7 @@ status_map = { "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", ], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ["On Hold", "eval:self.status=='On Hold'"], ], "Purchase Order": [ @@ -79,7 +79,7 @@ status_map = { ["Delivered", "eval:self.status=='Delivered'"], ["Cancelled", "eval:self.docstatus==2"], ["On Hold", "eval:self.status=='On Hold'"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Delivery Note": [ ["Draft", None], @@ -87,7 +87,7 @@ status_map = { ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Purchase Receipt": [ ["Draft", None], @@ -95,7 +95,7 @@ status_map = { ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], - ["Closed", "eval:self.status=='Closed'"], + ["Closed", "eval:self.status=='Closed' and self.docstatus != 2"], ], "Material Request": [ ["Draft", None], diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index 18e18dd31a9..ebf01bf43fb 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -173,7 +173,10 @@ class TestWebsiteItem(unittest.TestCase): # Website Item Portal Tests Begin def test_website_item_breadcrumbs(self): - "Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group." + """ + Check if breadcrumbs include homepage, product listing navigation page, + parent item group(s) and item group + """ from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups item_code = "Test Breadcrumb Item" @@ -196,7 +199,7 @@ class TestWebsiteItem(unittest.TestCase): breadcrumbs = get_parent_item_groups(item.item_group) self.assertEqual(breadcrumbs[0]["name"], "Home") - self.assertEqual(breadcrumbs[1]["name"], "Shop by Category") + self.assertEqual(breadcrumbs[1]["name"], "All Products") self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 47277cc48ce..f49e06675dd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -374,4 +374,4 @@ erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0}) -erpnext.patches.v13_0.update_schedule_type_in_loans \ No newline at end of file +erpnext.patches.v13_0.update_schedule_type_in_loans diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index f6427ca55a6..bf48d3ab04a 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after def execute(): doctypes_to_reload = [ + ("setup", "company"), ("stock", "repost_item_valuation"), ("stock", "stock_entry_detail"), ("stock", "purchase_receipt_item"), diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index c75e7b77f70..a9f1ff684f8 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -115,24 +115,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ calculate_item_values: function() { let me = this; if (!this.discount_amount_applied) { - $.each(this.frm.doc["items"] || [], function(i, item) { + for (item of this.frm.doc.items || []) { frappe.model.round_floats_in(item); item.net_rate = item.rate; - - if ((!item.qty) && me.frm.doc.is_return) { - item.amount = flt(item.rate * -1, precision("amount", item)); - } else if ((!item.qty) && me.frm.doc.is_debit_note) { - item.amount = flt(item.rate, precision("amount", item)); - } else { - item.amount = flt(item.rate * item.qty, precision("amount", item)); - } - - item.net_amount = item.amount; + item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; + item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); item.item_tax_amount = 0.0; item.total_weight = flt(item.weight_per_unit * item.stock_qty); me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]); - }); + } } }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 8bb25d4538c..a1c6cb5cd6c 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -546,6 +546,42 @@ class TestSalesOrder(FrappeTestCase): workflow.is_active = 0 workflow.save() + def test_bin_details_of_packed_item(self): + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + so = make_sales_order( + item_code="_Test Product Bundle Item New", + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + so.transaction_date = nowdate() + so.save() + + packed_item = so.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_update_child_product_bundle(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index b0ff1ccfcd7..a11cfc3407a 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -149,12 +149,12 @@ def get_item_for_list_in_html(context): def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} + base_nav_page = {"name": _("All Products"), "route": "/all-products"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] - if last_page and last_page in ("shop-by-category", "all-products"): + if last_page and last_page == "shop-by-category": base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index d747383d6a5..903e2af3cb3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(gle_warehouse_amount, 1400) + def test_bin_details_of_packed_item(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New"): + bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 1", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2) + + si = create_delivery_note( + item_code="_Test Product Bundle Item New", + update_stock=1, + warehouse="_Test Warehouse - _TC", + transaction_date=add_days(nowdate(), -1), + do_not_submit=1, + ) + + make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100) + + bin_details = frappe.db.get_value( + "Bin", + {"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "projected_qty", "ordered_qty"], + as_dict=1, + ) + + si.transaction_date = nowdate() + si.save() + + packed_item = si.packed_items[0] + self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty)) + self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty)) + self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty)) + def test_return_for_serialized_items(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] @@ -650,6 +690,11 @@ class TestDeliveryNote(FrappeTestCase): update_delivery_note_status(dn.name, "Closed") self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed") + # Check cancelling closed delivery note + dn.load_from_db() + dn.cancel() + self.assertEqual(dn.status, "Cancelled") + def test_dn_billing_status_case1(self): # SO -> DN -> SI so = make_sales_order() diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 391ff06918a..ac4c313e28a 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -74,11 +74,10 @@ class ItemAttribute(Document): def validate_duplication(self): values, abbrs = [], [] for d in self.item_attribute_values: - d.abbr = d.abbr.upper() - if d.attribute_value in values: - frappe.throw(_("{0} must appear only once").format(d.attribute_value)) + if d.attribute_value.lower() in map(str.lower, values): + frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title())) values.append(d.attribute_value) - if d.abbr in abbrs: - frappe.throw(_("{0} must appear only once").format(d.abbr)) + if d.abbr.lower() in map(str.lower, abbrs): + frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title())) abbrs.append(d.abbr) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 6fb9205d4b9..15d77544236 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -226,8 +226,10 @@ def validate_item_details(args, item): validate_end_of_life(item.name, item.end_of_life, item.disabled) - if args.transaction_type == "selling" and cint(item.has_variants): - throw(_("Item {0} is a template, please select one of its variants").format(item.name)) + if cint(item.has_variants): + msg = f"Item {item.name} is a template, please select one of its variants" + + throw(_(msg), title=_("Template Item Selected")) elif args.transaction_type == "buying" and args.doctype != "Material Request": if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1: