diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 0611f880c5e..a8776fa3448 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -386,7 +386,6 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): doc, credit_account, debit_account, - against, amount, base_amount, end_date, @@ -570,7 +569,6 @@ def book_revenue_via_journal_entry( doc, credit_account, debit_account, - against, amount, base_amount, posting_date, @@ -591,6 +589,7 @@ def book_revenue_via_journal_entry( journal_entry.voucher_type = ( "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" ) + journal_entry.process_deferred_accounting = deferred_process debit_entry = { "account": credit_account, diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 897151a97b7..445378300bb 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -205,10 +205,16 @@ def get_doctypes_with_dimensions(): return frappe.get_hooks("accounting_dimension_doctypes") -def get_accounting_dimensions(as_list=True): +def get_accounting_dimensions(as_list=True, filters=None): + + if not filters: + filters = {"disabled": 0} + if frappe.flags.accounting_dimensions is None: frappe.flags.accounting_dimensions = frappe.get_all( - "Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"] + "Accounting Dimension", + fields=["label", "fieldname", "disabled", "document_type"], + filters=filters, ) if as_list: diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 335fd350def..4493c722544 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -3,7 +3,7 @@ "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-03-25 10:53:52", + "creation": "2022-01-25 10:29:58.717206", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "process_deferred_accounting", "reversal_of", "tax_withholding_category", "column_break1", @@ -524,13 +525,20 @@ "label": "Reversal Of", "options": "Journal Entry", "read_only": 1 + }, + { + "fieldname": "process_deferred_accounting", + "fieldtype": "Link", + "label": "Process Deferred Accounting", + "options": "Process Deferred Accounting", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-01-04 13:39:36.485954", + "modified": "2022-04-06 17:18:46.865259", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -578,6 +586,7 @@ "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 08a7f4110f6..8ec726b36cd 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import ( convert_deferred_expense_to_expense, convert_deferred_revenue_to_income, ) -from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries class ProcessDeferredAccounting(Document): @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_reverse_gl_entries(gl_entries=gl_entries) + make_gl_entries(gl_entries=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6c38a7e597a..7781fe3391a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2240,6 +2240,14 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") + def test_deferred_revenue_missing_account(self): + si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True) + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-10" + si.items[0].service_end_date = "2019-03-15" + + self.assertRaises(frappe.ValidationError, si.save) + def test_fixed_deferred_revenue(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -3104,7 +3112,7 @@ class TestSalesInvoice(unittest.TestCase): acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.submit_journal_entries = 0 acc_settings.save() frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8a9318e184e..7cbd2bd6c7a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -180,6 +180,7 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.validate_deferred_income_expense_account() self.set_inter_company_account() if self.doctype == "Purchase Invoice": @@ -208,6 +209,27 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) + def validate_deferred_income_expense_account(self): + field_map = { + "Sales Invoice": "deferred_revenue_account", + "Purchase Invoice": "deferred_expense_account", + } + + for item in self.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + if not item.get(field_map.get(self.doctype)): + default_deferred_account = frappe.db.get_value( + "Company", self.company, "default_" + field_map.get(self.doctype) + ) + if not default_deferred_account: + frappe.throw( + _( + "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" + ).format(item.idx) + ) + else: + item.set(field_map.get(self.doctype), default_deferred_account) + def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index eda36868b9f..233b476a14b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -22,6 +22,9 @@ class QtyMismatchError(ValidationError): class BuyingController(StockController, Subcontracting): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] + def get_feed(self): if self.get("supplier_name"): return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7877827ac79..19fedb3c38f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -16,6 +16,9 @@ from erpnext.stock.utils import get_incoming_rate class SellingController(StockController): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] + def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js index 035290d8f19..5252798ba57 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js @@ -140,26 +140,6 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap } } - start_date(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - end_date(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - periodicity(doc, cdt, cdn) { - this.set_no_of_visits(doc, cdt, cdn); - } - - set_no_of_visits(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); - let me = this; - if (item.start_date && item.periodicity) { - me.frm.call('validate_end_date_visits'); - - } - } }; extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm})); diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 9a23c071061..04c080cc72e 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -213,6 +213,26 @@ class MaintenanceSchedule(TransactionBase): if chk: throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) + def validate_items_table_change(self): + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + for prev_item, item in zip(doc_before_save.items, self.items): + fields = [ + "item_code", + "start_date", + "end_date", + "periodicity", + "sales_person", + "no_of_visits", + "serial_no", + ] + for field in fields: + b_doc = prev_item.as_dict() + doc = item.as_dict() + if cstr(b_doc[field]) != cstr(doc[field]): + return True + def validate_no_of_visits(self): return len(self.schedules) != sum(d.no_of_visits for d in self.items) @@ -221,7 +241,7 @@ class MaintenanceSchedule(TransactionBase): self.validate_maintenance_detail() self.validate_dates_with_periodicity() self.validate_sales_order() - if not self.schedules or self.validate_no_of_visits(): + if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits(): self.generate_schedule() def on_update(self): diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index a98cd10e320..2268e0f7afa 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -123,6 +123,36 @@ class TestMaintenanceSchedule(unittest.TestCase): frappe.db.rollback() + def test_schedule_with_serials(self): + # Checks whether serials are automatically updated when changing in items table. + # Also checks if other fields trigger generate schdeule if changed in items table. + item_code = "_Test Serial Item" + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002") + ms.save() + + # Before Save + self.assertEqual(ms.schedules[0].serial_no, "TEST001, TEST002") + self.assertEqual(ms.schedules[0].sales_person, "Sales Team") + self.assertEqual(len(ms.schedules), 4) + self.assertFalse(ms.validate_items_table_change()) + # After Save + ms.items[0].serial_no = "TEST001" + ms.items[0].sales_person = "_Test Sales Person" + ms.items[0].no_of_visits = 2 + self.assertTrue(ms.validate_items_table_change()) + ms.save() + self.assertEqual(ms.schedules[0].serial_no, "TEST001") + self.assertEqual(ms.schedules[0].sales_person, "_Test Sales Person") + self.assertEqual(len(ms.schedules), 2) + # When user manually deleted a row from schedules table. + ms.schedules.pop() + self.assertEqual(len(ms.schedules), 1) + ms.save() + self.assertEqual(len(ms.schedules), 2) + + frappe.db.rollback() + def make_serial_item_with_serial(item_code): serial_item_doc = create_item(item_code, is_stock_item=1) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 72686e7403f..e2f6cb3a6cc 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', { // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { let item_code = frm.doc.purposes[0].item_code; + if (!item_code) { + return; + } frappe.call({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 8934f9c4e09..2aba48231be 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1144,6 +1144,56 @@ class TestWorkOrder(FrappeTestCase): for index, row in enumerate(ste_manu.get("items"), start=1): self.assertEqual(index, row.idx) + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_work_order_multiple_material_transfer(self): + """ + Test transferring multiple RMs in separate Stock Entries. + """ + work_order = make_wo_order_test_record(planned_start_date=now(), qty=1) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=5000.0, + ) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=2, + basic_rate=1000.0, + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1) + ) + del transfer_entry.get("items")[0] # transfer only one RM + transfer_entry.submit() + + # WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 0) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + + final_transfer_entry = frappe.get_doc( # transfer last RM with For Quantity = 0 + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + final_transfer_entry.save() + + self.assertEqual(final_transfer_entry.fg_completed_qty, 0.0) + self.assertEqual(final_transfer_entry.items[0].qty, 1) + + final_transfer_entry.submit() + work_order.reload() + + # WO's "Material Transferred for Mfg" shows all is transferred, no RM is pending + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 1) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 6433a992830..20f15039efe 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -540,8 +540,10 @@ erpnext.work_order = { || frm.doc.transfer_material_against == 'Job Card') ? 0 : 1; if (show_start_btn) { - if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) - && frm.doc.status != 'Stopped') { + let pending_to_transfer = frm.doc.required_items.some( + item => flt(item.transferred_qty) < flt(item.required_qty) + ); + if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { erpnext.work_order.create_pick_list(frm); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2ee848c356a..2802310250b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1186,7 +1186,11 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.from_bom = 1 stock_entry.bom_no = work_order.bom_no stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty)) + # accept 0 qty as well + stock_entry.fg_completed_qty = ( + qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) + ) + if work_order.bom_no: stock_entry.inspection_required = frappe.db.get_value( "BOM", work_order.bom_no, "inspection_required" diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 9999a6d167b..943bd2c3d20 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -78,8 +78,9 @@ frappe.query_reports["GSTR-1"] = { } }); - report.page.add_inner_button(__("Download as JSON"), function () { + let filters = report.get_values(); + frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1e624714d05..c4aa8a4711b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1803,7 +1803,9 @@ class StockEntry(StockController): or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") or allow_overproduction ): - item_dict[item]["qty"] = desire_to_transfer + # "No need for transfer but qty still pending to transfer" case can occur + # when transferring multiple RM in different Stock Entries + item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue else: