diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 617b376128..3cc28a3dc8 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 279557adc7..76d9cc7b49 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -537,8 +537,11 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: - for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): + stock_ledger_entries = frappe.get_all("Stock Ledger Entry", + fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], + filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0} + ) + for d in stock_ledger_entries: voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) valuation_tax_accounts = [d.account_head for d in self.get("taxes") diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index dc1f7aae42..f10a5eab10 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity): opening_balance = 0 float_precision = cint(frappe.db.get_default("float_precision")) or 2 if asset: - opening_balance = flt(asset[0].get("opening_balance", 0), float_precision) + opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision) if liability: - opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision) if equity: - opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision) opening_balance = flt(opening_balance, float_precision) if opening_balance: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 03ae0aea13..db28cdfdd3 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -282,7 +282,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row = { "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), - "currency": company_currency + "currency": company_currency, + "opening_balance": 0.0 } for row in out: @@ -294,6 +295,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row.setdefault("total", 0.0) total_row["total"] += flt(row["total"]) + total_row["opening_balance"] += row["opening_balance"] row["total"] = "" if "total" in total_row: diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 76a7cdab51..affde4aa8a 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -400,6 +400,16 @@ class StatusUpdater(Document): ref_doc = frappe.get_doc(ref_dt, ref_dn) ref_doc.db_set("per_billed", per_billed) + + # set billling status + if hasattr(ref_doc, 'billing_status'): + if ref_doc.per_billed < 0.001: + ref_doc.db_set("billing_status", "Not Billed") + elif ref_doc.per_billed > 99.999999: + ref_doc.db_set("billing_status", "Fully Billed") + else: + ref_doc.db_set("billing_status", "Partly Billed") + ref_doc.set_status(update=True) def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"): diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 8368db6374..e1e7225e05 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -172,10 +172,15 @@ class ProductionPlanReport(object): self.purchase_details = {} - for d in frappe.get_all("Purchase Order Item", + purchased_items = frappe.get_all("Purchase Order Item", fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], - filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}, - group_by = "item_code, warehouse"): + filters={ + "item_code": ("in", self.item_codes), + "warehouse": ("in", self.warehouses), + "docstatus": 1, + }, + group_by = "item_code, warehouse") + for d in purchased_items: key = (d.item_code, d.warehouse) if key not in self.purchase_details: self.purchase_details.setdefault(key, d) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index 5dd1d701f0..8df995769d 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -58,6 +58,7 @@ "width": "50%" }, { + "allow_on_submit": 1, "default": "Yes", "fieldname": "is_active", "fieldtype": "Select", @@ -232,10 +233,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-03-31 15:41:12.342380", + "modified": "2022-02-03 23:50:10.205676", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -271,5 +273,6 @@ ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index d48cd67c38..cb79cf8286 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -295,6 +295,10 @@ class GSTR3BReport(Document): inter_state_supply_details = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): + gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') + place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' + export_type = self.invoice_detail_map.get(inv, {}).get('export_type') + for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: @@ -302,9 +306,8 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value elif item_code in self.is_non_gst: self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value - elif rate == 0: + elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'): self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value - #self.report_dict['sup_details']['osup_zero'][key] += tax_amount else: if inv in self.cgst_sgst_invoices: tax_rate = rate/2 @@ -315,9 +318,6 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100) self.report_dict['sup_details']['osup_det']['txval'] += taxable_value - gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' - if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: inter_state_supply_details.setdefault((gst_category, place_of_supply), { diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 42bc0b70f8..acf048e116 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1375,6 +1375,30 @@ class TestSalesOrder(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + def test_zero_amount_sales_order_billing_status(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + so = make_sales_order(uom="Nos", do_not_save=1) + so.items[0].rate = 0 + so.save() + so.submit() + + self.assertEqual(so.net_total, 0) + self.assertEqual(so.billing_status, 'Not Billed') + + si = create_sales_invoice(qty=10, do_not_save=1) + si.price_list = '_Test Price List' + si.items[0].rate = 0 + si.items[0].price_list_rate = 0 + si.items[0].sales_order = so.name + si.items[0].so_detail = so.items[0].name + si.save() + si.submit() + + self.assertEqual(si.net_total, 0) + so.load_from_db() + self.assertEqual(so.billing_status, 'Fully Billed') + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 9204842b8f..df8cadd7f8 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -4,10 +4,11 @@ import frappe -from frappe.utils import flt +from frappe.utils import add_to_date, flt, now from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.utils import update_gl_entries_after from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, @@ -28,7 +29,8 @@ class TestLandedCostVoucher(ERPNextTestCase): "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", + "is_cancelled": 0, }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) @@ -41,14 +43,39 @@ class TestLandedCostVoucher(ERPNextTestCase): "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", + "is_cancelled": 0, }, fieldname=["qty_after_transaction", "stock_value"], as_dict=1) self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) - self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) + # assert after submit + self.assertPurchaseReceiptLCVGLEntries(pr) + + # Mess up cancelled SLE modified timestamp to check + # if they aren't effective in any business logic. + frappe.db.set_value("Stock Ledger Entry", + { + "is_cancelled": 1, + "voucher_type": pr.doctype, + "voucher_no": pr.name + }, + "is_cancelled", 1, + modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True) + ) + + items, warehouses = pr.get_items_and_warehouses() + update_gl_entries_after(pr.posting_date, pr.posting_time, + warehouses, items, company=pr.company) + + # reassert after reposting + self.assertPurchaseReceiptLCVGLEntries(pr) + + + def assertPurchaseReceiptLCVGLEntries(self, pr): + gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) @@ -74,8 +101,8 @@ class TestLandedCostVoucher(ERPNextTestCase): for gle in gl_entries: if not gle.get('is_cancelled'): - self.assertEqual(expected_values[gle.account][0], gle.debit) - self.assertEqual(expected_values[gle.account][1], gle.credit) + self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}") + self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}") def test_landed_cost_voucher_against_purchase_invoice(self): diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index e3b5795f4c..07c2f1f0dd 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -27,8 +27,7 @@ def make_packing_list(doc): stale_packed_items_table = get_indexed_packed_items_table(doc) - if not doc.is_new(): - reset = reset_packing_list_if_deleted_items_exist(doc) + reset = reset_packing_list(doc) for item_row in doc.get("items"): if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): @@ -64,20 +63,24 @@ def get_indexed_packed_items_table(doc): return indexed_table -def reset_packing_list_if_deleted_items_exist(doc): - doc_before_save = doc.get_doc_before_save() +def reset_packing_list(doc): + "Conditionally reset the table and return if it was reset or not." reset_table = False + doc_before_save = doc.get_doc_before_save() if doc_before_save: # reset table if: # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) - # we maintain list to maintain repeated item rows as well + # we maintain list to track recurring item rows as well items_before_save = [item.item_code for item in doc_before_save.get("items")] items_after_save = [item.item_code for item in doc.get("items")] reset_table = items_before_save != items_after_save else: - reset_table = True # reset if via Update Items (cannot determine action) + # reset: if via Update Items OR + # if new mapped doc with packed items set (SO -> DN) + # (cannot determine action) + reset_table = True if reset_table: doc.set("packed_items", []) diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index ed4eecde1d..5cbaa1ea66 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item from erpnext.tests.utils import ERPNextTestCase, change_settings @@ -22,7 +23,7 @@ class TestPackedItem(ERPNextTestCase): qty=2 ) - def test_sales_order_adding_bundle_item(self): + def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, do_not_submit=True) @@ -32,7 +33,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") self.assertEqual(so.packed_items[0].qty, 2) - def test_sales_order_updating_bundle_item(self): + def test_updating_bundle_item(self): "Test impact on packed items if bundle item row is updated." so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, do_not_submit=True) @@ -49,7 +50,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(so.packed_items), 0) - def test_sales_order_recurring_bundle_item(self): + def test_recurring_bundle_item(self): "Test impact on packed items if same bundle item is added and removed." so_items = [] for qty in [2, 4, 6, 8]: @@ -91,7 +92,7 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.packed_items[3].qty, 12) @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) - def test_sales_order_bundle_item_cumulative_price(self): + def test_bundle_item_cumulative_price(self): "Test if Bundle Item rate is cumulative from packed items." so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, do_not_submit=True) @@ -102,3 +103,25 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(so.items[0].rate, 350) self.assertEqual(so.items[0].amount, 700) + + def test_newly_mapped_doc_packed_items(self): + "Test impact on packed items in newly mapped DN from SO." + so_items = [] + for qty in [2, 4]: + so_items.append({ + "item_code": "_Test Product Bundle X", + "qty": qty, + "rate": 400, + "warehouse": "_Test Warehouse - _TC" + }) + + # create SO with recurring bundle item + so = make_sales_order(item_list=so_items) + + dn = make_delivery_note(so.name) + dn.items[1].qty = 3 # change second row qty for inserting doc + dn.save() + + self.assertEqual(len(dn.packed_items), 4) + self.assertEqual(dn.packed_items[2].qty, 6) + self.assertEqual(dn.packed_items[3].qty, 6) \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1257057ea3..ffdf8c420c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -286,7 +286,7 @@ class PurchaseReceipt(BuyingController): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name, "warehouse": d.warehouse}, "stock_value_difference") + "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") if not stock_value_diff: continue