diff --git a/codecov.yml b/codecov.yml index 67bd4454ef..1fa602ab30 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,6 +8,16 @@ coverage: target: auto threshold: 0.5% + patch: + default: + target: 85% + threshold: 0% + base: auto + branches: + - develop + if_ci_failed: ignore + only_pulls: true + comment: layout: "diff, files" require_changes: true diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f4fd1bf16e..df957d261b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -297,8 +297,15 @@ class PurchaseInvoice(BuyingController): item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): - item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, + asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) + if not asset_category_account: + form_link = get_link_to_form('Asset Category', asset_category) + throw( + _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), + title=_("Missing Account") + ) + item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index c2b1bbcf14..153f5c537a 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 333906a815..a18b03a888 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -185,83 +185,84 @@ class Asset(AccountsController): if not self.available_for_use_date: return - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + start = self.clear_depreciation_schedule() - start = self.clear_depreciation_schedule() + for finance_book in self.get('finance_books'): + self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value - if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = flt(d.value_after_depreciation) + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = flt(finance_book.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(d) + has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: number_of_pending_depreciations += 1 skip_row = False - for n in range(start, number_of_pending_depreciations): + + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: - from_date = self.get_from_date(d.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, from_date, date_of_sale) if depreciation_amount > 0: self.append("schedules", { "schedule_date": date_of_sale, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) break # For first row if has_pro_rata and not self.opening_accumulated_depreciation and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(d, + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, schedule_date, self.to_date) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, d.finance_book) + depreciation_amount, finance_book.finance_book) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) @@ -272,10 +273,10 @@ class Asset(AccountsController): self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != d.expected_value_after_useful_life) - or value_after_depreciation < d.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) skip_row = True if depreciation_amount > 0: @@ -285,7 +286,7 @@ class Asset(AccountsController): # In pro rata case, for first and last depreciation, month range would be different month_range = months \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else d.frequency_of_depreciation + else finance_book.frequency_of_depreciation for r in range(month_range): if (has_pro_rata and n == 0): @@ -311,27 +312,52 @@ class Asset(AccountsController): self.append("schedules", { "schedule_date": date, "depreciation_amount": amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) else: self.append("schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) - # used when depreciation schedule needs to be modified due to increase in asset life + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales + # JE: Journal Entry, FB: Finance Book def clear_depreciation_schedule(self): - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + start = [] + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get('schedules'): + + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + if len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to ensure that start will only be updated once for each FB + if len(start) == (int(schedule.finance_book_id) - 1): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when all the schedule rows corresponding to the last FB are linked with JEs + if len(start) == (len(self.finance_books) - 1): + start.append(num_of_depreciations_completed) + + # when the Depreciation Schedule is being created for the first time + if start == []: + start = [0] * len(self.finance_books) + else: + self.schedules = depr_schedule + return start def get_from_date(self, finance_book): @@ -469,7 +495,6 @@ class Asset(AccountsController): asset_value_after_full_schedule = flt( flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation) - flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) if (row.expected_value_after_useful_life and @@ -731,14 +756,14 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") - asset_maintenance.update({ +def create_asset_value_adjustment(asset, asset_category, company): + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index ce2cb01ab2..44c4ce542d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -955,6 +955,82 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_clear_depreciation_schedule_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 1, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-01-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 1, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-01-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.submit() + + post_depreciation_entries(date="2020-04-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 6) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, "1") + else: + self.assertEqual(schedule.finance_book_id, "2") + + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.save() + + self.assertEqual(len(asset.schedules), 9) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, 1) + else: + self.assertEqual(schedule.finance_book_id, 2) + def test_depreciation_entry_cancellation(self): asset = create_asset( item_code = "Macbook Pro", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index bde00cbd94..e7049fd522 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{ dialog.show() }, + schedule_date(frm) { + if(frm.doc.schedule_date){ + frm.doc.items.forEach((item) => { + item.schedule_date = frm.doc.schedule_date; + }) + } + refresh_field("items"); + }, preview: (frm) => { let dialog = new frappe.ui.Dialog({ title: __('Preview Email'), @@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{ dialog.show(); } }) - +frappe.ui.form.on("Request for Quotation Item", { + items_add(frm, cdt, cdn) { + if (frm.doc.schedule_date) { + frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date); + } + } +}); frappe.ui.form.on("Request for Quotation Supplier",{ supplier: function(frm, cdt, cdn) { var d = locals[cdt][cdn] diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 4ce4100a7f..4993df90d1 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -12,6 +12,7 @@ "vendor", "column_break1", "transaction_date", + "schedule_date", "status", "amended_from", "suppliers_section", @@ -246,16 +247,22 @@ "fieldname": "sec_break_email_2", "fieldtype": "Section Break", "hide_border": 1 + }, + { + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required Date" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 22:04:29.017134", + "modified": "2021-11-24 17:47:49.909000", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index a0bf9b32ec..8e076edf0b 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -19,7 +19,7 @@ class TestExitInterview(unittest.TestCase): frappe.db.sql('delete from `tabExit Interview`') def test_duplicate_interview(self): - employee = make_employee('employeeexit1@example.com') + employee = make_employee('employeeexitint1@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) interview = create_exit_interview(employee) @@ -27,7 +27,7 @@ class TestExitInterview(unittest.TestCase): self.assertRaises(frappe.DuplicateEntryError, doc.save) def test_relieving_date_validation(self): - employee = make_employee('employeeexit2@example.com') + employee = make_employee('employeeexitint2@example.com') # unset relieving date frappe.db.set_value('Employee', employee, 'relieving_date', None) diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 8e0b07d3e1..bde5a89926 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -108,12 +108,11 @@ def get_data(filters): interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement')) .distinct() - .orderby(employee.relieving_date, order=Order.asc) .where( ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) - ) + ).orderby(employee.relieving_date, order=Order.asc) ) query = get_conditions(filters, query, employee, interview, fnf) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index d5db3fc1cc..eff2344e85 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -1,17 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.utils import add_months, today from erpnext import get_company_currency +from erpnext.tests.utils import ERPNextTestCase from .blanket_order import make_order -class TestBlanketOrder(unittest.TestCase): +class TestBlanketOrder(ERPNextTestCase): def setUp(self): frappe.flags.args = frappe._dict() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 4c032307d8..178d92c26c 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest from collections import deque from functools import partial @@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.tests.test_subcontracting import set_backflush_based_on +from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOM(unittest.TestCase): +class TestBOM(ERPNextTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 526c243ed7..12576cbf32 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -1,19 +1,16 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - - -import unittest - import frappe from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOMUpdateTool(unittest.TestCase): +class TestBOMUpdateTool(ERPNextTestCase): def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 453ad50e8e..dac7b36f94 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -75,6 +75,15 @@ frappe.ui.form.on('Job Card', { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } + + if (frm.doc.work_order) { + frappe.db.get_value('Work Order', frm.doc.work_order, + 'transfer_material_against').then((r) => { + if (r.message.transfer_material_against == 'Work Order') { + frm.set_df_property('items', 'hidden', 1); + } + }); + } }, setup_corrective_job_card: function(frm) { diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 9b4fc8b8b7..bb5004ba86 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1,6 +1,5 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.utils import random_string @@ -12,9 +11,10 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestCase -class TestJobCard(unittest.TestCase): +class TestJobCard(ERPNextTestCase): def setUp(self): make_bom_for_jc_tests() @@ -329,4 +329,4 @@ def make_bom_for_jc_tests(): bom.rm_cost_as_per = "Valuation Rate" bom.items[0].uom = "_Test UOM 1" bom.items[0].conversion_factor = 5 - bom.insert() \ No newline at end of file + bom.insert() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index a2980a732e..2febc1e23c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1,8 +1,5 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.utils import add_to_date, flt, now_datetime, nowdate @@ -17,9 +14,10 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.tests.utils import ERPNextTestCase -class TestProductionPlan(unittest.TestCase): +class TestProductionPlan(ERPNextTestCase): def setUp(self): for item in ['Test Production Item 1', 'Subassembly Item 1', 'Raw Material Item 1', 'Raw Material Item 2']: diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 68d9dec04b..e90b0a7d6d 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -1,17 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.test_runner import make_test_records from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestCase -class TestRouting(unittest.TestCase): +class TestRouting(ERPNextTestCase): @classmethod def setUpClass(cls): cls.item_code = "Test Routing Item - A" diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 1e74b6dda2..165e0acebb 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1,6 +1,5 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest import frappe from frappe.utils import add_months, cint, flt, now, today @@ -95,7 +94,7 @@ class TestWorkOrder(ERPNextTestCase): def test_reserved_qty_for_partial_completion(self): item = "_Test Item" - warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC") + warehouse = "_Test Warehouse - _TC" bin1_at_start = get_bin(item, warehouse) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 5ed5153528..c298c0a8db 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,8 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt - -import unittest - import frappe from frappe.test_runner import make_test_records @@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( WorkstationHolidayError, check_if_within_operating_hours, ) +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') make_test_records('Workstation') -class TestWorkstation(unittest.TestCase): +class TestWorkstation(ERPNextTestCase): def test_validate_timings(self): check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0687b42631..65ad79ef23 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -165,6 +165,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign +erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.rename_pos_closing_doctype @@ -287,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost -erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index 0bb86e0450..f4f9b17fb8 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.model.utils.rename_field import rename_field @@ -12,5 +13,20 @@ def execute(): if frappe.db.exists('DocType', 'Sales Invoice'): frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + + # rename_field method assumes that the field already exists or the doc is synced + if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'): + create_custom_fields({ + 'Sales Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ] + }) + if frappe.db.has_column('Sales Invoice', 'qr_code'): rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code") diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 05af09e49d..b035292c0b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -940,10 +940,12 @@ class SalarySlip(TransactionBase): def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): amount, additional_amount = row.amount, row.additional_amount + timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component") + if (self.salary_structure and cint(row.depends_on_payment_days) and cint(self.total_working_days) and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary - and (not self.salary_slip_based_on_timesheet or + and (row.salary_component != timesheet_component or getdate(self.start_date) < joining_date or (relieving_date and getdate(self.end_date) > relieving_date) )): @@ -952,7 +954,7 @@ class SalarySlip(TransactionBase): amount = flt((flt(row.default_amount) * flt(self.payment_days) / cint(self.total_working_days)), row.precision("amount")) + additional_amount - elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days): + elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days): amount, additional_amount = 0, 0 elif not row.amount: amount = flt(row.default_amount) + flt(row.additional_amount) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index e4618c31f2..3052a2b727 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_payment_days_in_salary_slip_based_on_timesheet(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + from erpnext.projects.doctype.timesheet.test_timesheet import ( + make_salary_structure_for_timesheet, + make_timesheet, + ) + from erpnext.projects.doctype.timesheet.timesheet import ( + make_salary_slip as make_salary_slip_for_timesheet, + ) + + # Payroll based on attendance + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") + + emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company") + frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) + + # mark attendance + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + + # salary structure based on timesheet + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, is_billable=1) + salary_slip = make_salary_slip_for_timesheet(timesheet.name) + salary_slip.start_date = month_start_date + salary_slip.end_date = month_end_date + salary_slip.save() + salary_slip.submit() + + no_of_days = self.get_no_of_days() + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1) + + # gross pay calculation based on attendance (payment days) + gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days)) + + self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) + + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_component_amount_dependent_on_another_payment_days_based_component(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index d59cc01013..148d8ba29c 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]: frappe.db.sql("delete from `tab%s`" % dt) - if not frappe.db.exists("Salary Component", "Timesheet Component"): - frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() - - def test_timesheet_billing_amount(self): emp = make_employee("test_employee_6@salary.com") @@ -160,6 +156,9 @@ def make_salary_structure_for_timesheet(employee, company=None): salary_structure_name = "Timesheet Salary Structure Test" frequency = "Monthly" + if not frappe.db.exists("Salary Component", "Timesheet Component"): + frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() + salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True) salary_structure.salary_component = "Timesheet Component" salary_structure.salary_slip_based_on_timesheet = 1 diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index e03ad374ae..1c1335ebe0 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -114,9 +114,11 @@ def get_items(filters): items = frappe.db.sql(""" select - `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, - `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, - `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount, + `tabSales Invoice Item`.gst_hsn_code, + `tabSales Invoice Item`.stock_uom, + sum(`tabSales Invoice Item`.stock_qty) as stock_qty, + sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, + sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, `tabGST HSN Code`.description from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` @@ -124,6 +126,8 @@ def get_items(filters): and `tabSales Invoice`.docstatus = 1 and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s + group by + `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code """ % (conditions, match_conditions), filters, as_dict=1) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py new file mode 100644 index 0000000000..86dc458bdb --- /dev/null +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py @@ -0,0 +1,89 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +from unittest import TestCase + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + make_company as setup_company, +) +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + make_customers as setup_customers, +) +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + set_account_heads as setup_gst_settings, +) +from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import ( + execute as run_report, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestHSNWiseSummaryReport(TestCase): + @classmethod + def setUpClass(cls): + setup_company() + setup_customers() + setup_gst_settings() + make_item("Golf Car", properties={ "gst_hsn_code": "999900" }) + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_hsn_summary_for_invoice_with_duplicate_items(self): + si = create_sales_invoice( + company="_Test Company GST", + customer = "_Test GST Customer", + currency = "INR", + warehouse = "Finished Goods - _GST", + debit_to = "Debtors - _GST", + income_account = "Sales - _GST", + expense_account = "Cost of Goods Sold - _GST", + cost_center = "Main - _GST", + do_not_save=1 + ) + + si.items = [] + si.append("items", { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "120", + "cost_center": "Main - _GST" + }) + si.append("items", { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "140", + "cost_center": "Main - _GST" + }) + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18 + }) + si.posting_date = "2020-11-17" + si.submit() + si.reload() + + [columns, data] = run_report(filters=frappe._dict({ + "company": "_Test Company GST", + "gst_hsn_code": "999900", + "company_gstin": si.company_gstin, + "from_date": si.posting_date, + "to_date": si.posting_date + })) + + filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data)) + self.assertTrue(filtered_rows) + + hsn_row = filtered_rows[0] + self.assertEquals(hsn_row['stock_qty'], 2.0) + self.assertEquals(hsn_row['total_amount'], 306.8) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7d6b74d066..5301fd0524 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -2,8 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.test_runner import make_test_records from frappe.utils import flt @@ -11,7 +9,7 @@ from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding -from erpnext.tests.utils import create_test_contact_and_address +from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address test_ignore = ["Price List"] test_dependencies = ['Payment Term', 'Payment Terms Template'] @@ -19,7 +17,7 @@ test_records = frappe.get_test_records('Customer') -class TestCustomer(unittest.TestCase): +class TestCustomer(ERPNextTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index 874a364592..b951044f33 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -6,6 +6,7 @@ import unittest import frappe from erpnext.controllers.queries import item_query +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ['Item', 'Customer', 'Supplier'] @@ -17,7 +18,7 @@ def create_party_specific_item(**args): psi.based_on_value = args.get('based_on_value') psi.insert() -class TestPartySpecificItem(unittest.TestCase): +class TestPartySpecificItem(ERPNextTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") self.supplier = frappe.get_last_doc("Supplier") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index aa83726304..4357201d23 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1,15 +1,15 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.utils import add_days, add_months, flt, getdate, nowdate +from erpnext.tests.utils import ERPNextTestCase + test_dependencies = ["Product Bundle"] -class TestQuotation(unittest.TestCase): +class TestQuotation(ERPNextTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get('payment_schedule')) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 2a0752e56a..42bc0b70f8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt import json -import unittest import frappe import frappe.permissions @@ -28,12 +27,14 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestCase -class TestSalesOrder(unittest.TestCase): +class TestSalesOrder(ERPNextTestCase): @classmethod def setUpClass(cls): + super().setUpClass() cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order")) @@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase): # reset config to previous state frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + super().tearDownClass() def tearDown(self): frappe.set_user("Administrator") diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index 9c30afc5b1..d62915fc66 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -2,8 +2,6 @@ # For license information, please see license.txt -import unittest - from frappe.utils import add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_material_request @@ -11,9 +9,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import ( execute, ) +from erpnext.tests.utils import ERPNextTestCase -class TestPendingSOItemsForPurchaseRequest(unittest.TestCase): +class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase): def test_result_for_partial_material_request(self): so = make_sales_order() mr=make_material_request(so.name) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 8ffc5d6d0a..f56cce2dfd 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -2,15 +2,14 @@ # For license information, please see license.txt -import unittest - import frappe from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.sales_analytics.sales_analytics import execute +from erpnext.tests.utils import ERPNextTestCase -class TestAnalytics(unittest.TestCase): +class TestAnalytics(ERPNextTestCase): def test_sales_analytics(self): frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index a33134b491..37b54116a4 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -43,9 +43,9 @@ class Bin(Document): frappe.qb .from_(wo) .from_(wo_item) - .select(Case() - .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty)) - .else_(Sum(wo_item.required_qty - wo_item.consumed_qty)) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) ) .where( (wo_item.item_code == self.item_code) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4f4e69105a..29abd45fcc 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -361,8 +361,7 @@ "fieldname": "valuation_method", "fieldtype": "Select", "label": "Valuation Method", - "options": "\nFIFO\nMoving Average", - "set_only_once": 1 + "options": "\nFIFO\nMoving Average" }, { "depends_on": "is_stock_item", @@ -1035,7 +1034,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-03 08:32:03.869294", + "modified": "2021-12-14 04:13:16.857534", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 29c4193f9e..4270839bfd 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -1,451 +1,140 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-04-08 13:10:16", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-04-08 13:10:16", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "batch_no", + "desc_section", + "description", + "quantity_section", + "qty", + "net_weight", + "column_break_10", + "stock_uom", + "weight_uom", + "page_break", + "dn_detail" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "options": "item_code.item_name", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "print_width": "200px", + "read_only": 1, "width": "200px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "batch_no", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Batch No", - "length": 0, - "no_copy": 0, - "options": "Batch", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "desc_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "desc_section", + "fieldtype": "Section Break", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Quantity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "quantity_section", + "fieldtype": "Section Break", + "label": "Quantity" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Quantity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "net_weight", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Net Weight", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "net_weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Net Weight", + "print_width": "100px", "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "weight_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Weight UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "weight_uom", + "fieldtype": "Link", + "label": "Weight UOM", + "options": "UOM", + "print_width": "100px", "width": "100px" - }, + }, { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "page_break", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Page Break", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Page Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dn_detail", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "DN Detail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "dn_detail", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "DN Detail" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-01 07:21:58.220980", - "modified_by": "Administrator", - "module": "Stock", - "name": "Packing Slip Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-12-14 01:22:00.715935", + "modified_by": "Administrator", + "module": "Stock", + "name": "Packing Slip Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 314f1608fa..3f490653e1 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -48,6 +48,7 @@ def get_item_info(filters): conditions = [get_item_group_condition(filters.get("item_group"))] if filters.get("brand"): conditions.append("item.brand=%(brand)s") + conditions.append("is_stock_item = 1") return frappe.db.sql("""select name, item_name, description, brand, item_group, safety_stock, lead_time_days from `tabItem` item where {}""" diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 50f31fde2d..c94700bdc5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -420,13 +420,13 @@ def handle_status_change(doc, apply_sla_for_resolution): if is_open_status(prev_status) and is_hold_status(doc.status): # Issue is on hold -> Set on_hold_since doc.on_hold_since = now_time + reset_expected_response_and_resolution(doc) # Replied to Open if is_hold_status(prev_status) and is_open_status(doc.status): # Issue was on hold -> Calculate Total Hold Time calculate_hold_hours() # Issue is open -> reset resolution_date - reset_expected_response_and_resolution(doc) reset_resolution_metrics(doc) # Open to Closed @@ -440,7 +440,6 @@ def handle_status_change(doc, apply_sla_for_resolution): # Issue was closed -> Calculate Total Hold Time from resolution_date calculate_hold_hours() # Issue is open -> reset resolution_date - reset_expected_response_and_resolution(doc) reset_resolution_metrics(doc) # Closed to Replied @@ -449,6 +448,7 @@ def handle_status_change(doc, apply_sla_for_resolution): calculate_hold_hours() # Issue is on hold -> Set on_hold_since doc.on_hold_since = now_time + reset_expected_response_and_resolution(doc) # Replied to Closed if is_hold_status(prev_status) and is_fulfilled_status(doc.status): @@ -640,28 +640,35 @@ def on_communication_update(doc, status): if not parent.meta.has_field('service_level_agreement'): return - for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - if ( doc.sent_or_received == "Received" # a reply is received and parent.get('status') == 'Open' # issue status is set as open from communication.py - and parent._doc_before_save + and parent.get_doc_before_save() and parent.get('status') != parent._doc_before_save.get('status') # status changed ): # undo the status change in db # since prev status is fetched from db - frappe.db.set_value(parent.doctype, parent.name, 'status', parent._doc_before_save.get('status')) + frappe.db.set_value( + parent.doctype, parent.name, + 'status', parent._doc_before_save.get('status'), + update_modified=False + ) elif ( doc.sent_or_received == "Sent" # a reply is sent and parent.get('first_responded_on') # first_responded_on is set from communication.py - and parent._doc_before_save + and parent.get_doc_before_save() and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set ): # reset first_responded_on since it will be handled/set later on parent.first_responded_on = None parent.flags.on_first_reply = True + else: + return + + for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) @@ -670,12 +677,10 @@ def on_communication_update(doc, status): def reset_expected_response_and_resolution(doc): - update_values = {} if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): - update_values['response_by'] = None + doc.response_by = None if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): - update_values['resolution_by'] = None - doc.db_set(update_values) + doc.resolution_by = None def set_response_by(doc, start_date_time, priority):