diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 14cbc5916c7..33fbd748c7b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -295,8 +295,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/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 5942ce5d637..744b99807fc 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -24,6 +24,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate +from erpnext.accounts.party import get_party_account_currency class Subscription(Document): @@ -356,6 +357,9 @@ class Subscription(Document): if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'): invoice.apply_tds = 1 + ### Add party currency to invoice + invoice.currency = get_party_account_currency(self.party_type, self.party, self.company) + ## Add dimensions in invoice for subscription: accounting_dimensions = get_accounting_dimensions() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 9dd370bd472..6f67bc5128b 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -60,15 +60,38 @@ def create_plan(): plan.billing_interval_count = 3 plan.insert() + if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'): + plan = frappe.new_doc('Subscription Plan') + plan.plan_name = '_Test Plan Multicurrency' + plan.item = '_Test Non Stock Item' + plan.price_determination = "Fixed Rate" + plan.cost = 50 + plan.currency = 'USD' + plan.billing_interval = 'Month' + plan.billing_interval_count = 1 + plan.insert() + +def create_parties(): if not frappe.db.exists('Supplier', '_Test Supplier'): supplier = frappe.new_doc('Supplier') supplier.supplier_name = '_Test Supplier' supplier.supplier_group = 'All Supplier Groups' supplier.insert() + if not frappe.db.exists('Customer', '_Test Subscription Customer'): + customer = frappe.new_doc('Customer') + customer.customer_name = '_Test Subscription Customer' + customer.billing_currency = 'USD' + customer.append('accounts', { + 'company': '_Test Company', + 'account': '_Test Receivable USD - _TC' + }) + customer.insert() + class TestSubscription(unittest.TestCase): def setUp(self): create_plan() + create_parties() def test_create_subscription_with_trial_with_correct_period(self): subscription = frappe.new_doc('Subscription') @@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase): subscription.process() self.assertEqual(len(subscription.invoices), 1) + + def test_multicurrency_subscription(self): + subscription = frappe.new_doc('Subscription') + subscription.party_type = 'Customer' + subscription.party = '_Test Subscription Customer' + subscription.generate_invoice_at_period_start = 1 + subscription.company = '_Test Company' + # select subscription start date as '2018-01-15' + subscription.start_date = '2018-01-01' + subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1}) + subscription.save() + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, 'Unpaid') + + # Check the currency of the created invoice + currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency') + self.assertEqual(currency, 'USD') \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 353f9087f1b..a990f23cd6b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -545,7 +545,9 @@ class ReceivablePayableReport(object): def set_ageing(self, row): if self.filters.ageing_based_on == "Due Date": - entry_date = row.due_date + # use posting date as a fallback for advances posted via journal and payment entry + # when ageing viewed by due date + entry_date = row.due_date or row.posting_date elif self.filters.ageing_based_on == "Supplier Invoice Date": entry_date = row.bill_date else: diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index a3a45d1e79c..caee1a10bbb 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): posting_date = entry.posting_date voucher_type = entry.voucher_type + if not tax_withholding_category: + tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + rate = tax_rate_map.get(tax_withholding_category) + if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit) - if rate and tds_deducted: + if tds_deducted: row = { 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'supplier': supplier_map.get(supplier, {}).get('name') @@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) + suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) for d in suppliers: supplier_map[d.name] = d diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index c2b1bbcf142..153f5c537a2 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 beb46f37ab1..90257c784ee 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -470,7 +470,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 @@ -732,14 +731,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/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index b76c3784a47..e0ddc722cc9 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 4ce4100a7fc..4993df90d16 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/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json index ae1f36e73fa..6afd3f70381 100644 --- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json @@ -115,8 +115,7 @@ ], "issingle": 1, "links": [], - "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f", - "modified": "2021-11-30 11:17:24.647979", + "modified": "2021-11-30 12:17:24.647979", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "TaxJar Settings", diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 37ea3fdac30..501712613a8 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -56,9 +56,14 @@ class TestMaintenanceSchedule(unittest.TestCase): ms.submit() s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1]) - test = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id) + + # Check if item is mapped in visit. + test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id) + self.assertEqual(len(test_map_visit.purposes), 1) + self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item") + visit = frappe.new_doc('Maintenance Visit') - visit = test + visit = test_map_visit visit.maintenance_schedule = ms.name visit.maintenance_schedule_detail = s_id visit.completion_status = "Partially Completed" diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index b920b6339ac..78289679744 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Maintenance Visit', { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { - frm.clear_table("purposes"); + frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes"); frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index d5db3fc1ccf..eff2344e85c 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 6126c95cd45..2f9804d1d4a 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 526c243ed78..12576cbf322 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 bb258812b21..768cea33dae 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -76,6 +76,15 @@ frappe.ui.form.on('Job Card', { frm.trigger("prepare_timer_buttons"); } frm.trigger("setup_quality_inspection"); + + 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_quality_inspection: 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 9b4fc8b8b7a..bb5004ba86f 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 a2980a732ed..2febc1e23c0 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 68d9dec04b1..e90b0a7d6d2 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 f4a88dc4598..3949fe34852 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 @@ -21,9 +20,10 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin +from erpnext.tests.utils import ERPNextTestCase, timeout -class TestWorkOrder(unittest.TestCase): +class TestWorkOrder(ERPNextTestCase): def setUp(self): self.warehouse = '_Test Warehouse 2 - _TC' self.item = '_Test Item' @@ -91,7 +91,7 @@ class TestWorkOrder(unittest.TestCase): 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) @@ -376,6 +376,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(len(ste.additional_costs), 1) self.assertEqual(ste.total_additional_costs, 1000) + @timeout(seconds=60) def test_job_card(self): stock_entries = [] bom = frappe.get_doc('BOM', { @@ -769,6 +770,7 @@ class TestWorkOrder(unittest.TestCase): total_pl_qty ) + @timeout(seconds=60) def test_job_card_scrap_item(self): items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 5ed51535282..c298c0a8dbb 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 f2c0ff5ca6b..47d424b3524 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -178,6 +178,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.update_appointment_reminder_scheduler_entry @@ -311,7 +312,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category") 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_website_items #30-09-2021 erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.make_homepage_products_website_items @@ -336,5 +336,5 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.rename_ksa_qr_field -erpnext.patches.v13_0.disable_ksa_print_format_for_others -erpnext.patches.v13_0.update_tax_category_for_rcm \ No newline at end of file +erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 +erpnext.patches.v13_0.update_tax_category_for_rcm diff --git a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py index c815b3bb3c9..aa2a2d3b785 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -3,10 +3,13 @@ import frappe +from erpnext.regional.saudi_arabia.setup import add_print_formats + def execute(): company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) if company: + add_print_formats() return if frappe.db.exists('DocType', 'Print Format'): diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index 0bb86e04509..f4f9b17fb81 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 6e9503fb9a6..efbc4681d7a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -944,10 +944,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) )): @@ -956,7 +958,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 0f97b7aaf85..e621322d940 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 d59cc01013b..148d8ba29c2 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 17d611738d3..ac78a8107df 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 @@ -115,9 +115,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` @@ -125,6 +127,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 00000000000..86dc458bdb1 --- /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 80f618ce928..7802a3fea44 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'] @@ -20,7 +18,7 @@ test_records = frappe.get_test_records('Customer') from six import iteritems -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 874a3645929..b951044f332 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 aa83726304f..4357201d23d 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 7e6040c38f0..9c0150ef77c 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 @@ -22,12 +21,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")) @@ -36,6 +37,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 9c30afc5b1a..d62915fc66d 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 8ffc5d6d0a7..f56cce2dfdc 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 17c8367a638..27e83caf99b 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -33,10 +33,10 @@ class Bin(Document): in open work orders''' self.reserved_qty_for_production = frappe.db.sql(''' SELECT - CASE WHEN ifnull(skip_transfer, 0) = 0 THEN - SUM(item.required_qty - item.transferred_qty) + SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN + item.required_qty - item.transferred_qty ELSE - SUM(item.required_qty - item.consumed_qty) + item.required_qty - item.consumed_qty END) END FROM `tabWork Order` pro, `tabWork Order Item` item WHERE diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 39a94a0868b..d364d8a7d95 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -359,8 +359,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", @@ -956,7 +955,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 29c4193f9e9..4270839bfdb 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 314f1608fa1..3f490653e14 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/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 963c4489e44..7d65e1ab03c 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -168,7 +168,7 @@ def get_stock_ledger_entries(filters, items): sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no from - `tabStock Ledger Entry` sle force index (posting_sort_index) + `tabStock Ledger Entry` sle where sle.docstatus < 2 %s %s and is_cancelled = 0 order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec diff --git a/erpnext/stock/report/stock_ledger_invariant_check/__init__.py b/erpnext/stock/report/stock_ledger_invariant_check/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js new file mode 100644 index 00000000000..c484516a163 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -0,0 +1,43 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +const DIFFERNCE_FIELD_NAMES = [ + "difference_in_qty", + "fifo_qty_diff", + "fifo_value_diff", + "fifo_valuation_diff", + "valuation_diff", + "fifo_difference_diff" +]; + +frappe.query_reports["Stock Ledger Invariant Check"] = { + "filters": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item", + "mandatory": 1, + "options": "Item", + get_query: function() { + return { + filters: {is_stock_item: 1, has_serial_no: 0} + } + } + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "mandatory": 1, + "options": "Warehouse", + } + ], + formatter (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { + value = "" + value + ""; + } + return value; + }, +}; diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json new file mode 100644 index 00000000000..d28fe0f62d1 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-16 06:31:23.290916", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-16 09:55:58.341764", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Ledger Invariant Check", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Stock Ledger Invariant Check", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py new file mode 100644 index 00000000000..ca47a1ec5b8 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -0,0 +1,236 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# License: GNU GPL v3. See LICENSE + +import json + +import frappe + +SLE_FIELDS = ( + "name", + "posting_date", + "posting_time", + "creation", + "voucher_type", + "voucher_no", + "actual_qty", + "qty_after_transaction", + "incoming_rate", + "outgoing_rate", + "stock_queue", + "batch_no", + "stock_value", + "stock_value_difference", + "valuation_rate", +) + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + + +def get_data(filters): + sles = get_stock_ledger_entries(filters) + return add_invariant_check_fields(sles) + + +def get_stock_ledger_entries(filters): + return frappe.get_all( + "Stock Ledger Entry", + fields=SLE_FIELDS, + filters={ + "item_code": filters.item_code, + "warehouse": filters.warehouse, + "is_cancelled": 0 + }, + order_by="timestamp(posting_date, posting_time), creation", + ) + + +def add_invariant_check_fields(sles): + balance_qty = 0.0 + for idx, sle in enumerate(sles): + queue = json.loads(sle.stock_queue) + + fifo_qty = 0.0 + fifo_value = 0.0 + for qty, rate in queue: + fifo_qty += qty + fifo_value += qty * rate + + balance_qty += sle.actual_qty + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + balance_qty = sle.qty_after_transaction + + sle.fifo_queue_qty = fifo_qty + sle.fifo_stock_value = fifo_value + sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None + sle.balance_value_by_qty = ( + sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None + ) + sle.expected_qty_after_transaction = balance_qty + + # set difference fields + sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction + sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty + sle.fifo_value_diff = sle.stock_value - fifo_value + sle.fifo_valuation_diff = ( + sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None + ) + sle.valuation_diff = ( + sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None + ) + + if idx > 0: + sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value + sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference + + return sles + + +def get_columns(): + return [ + { + "fieldname": "name", + "fieldtype": "Link", + "label": "Stock Ledger Entry", + "options": "Stock Ledger Entry", + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + }, + { + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + }, + { + "fieldname": "creation", + "fieldtype": "Datetime", + "label": "Creation", + }, + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType", + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type", + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch", + "options": "Batch", + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Qty Change", + }, + { + "fieldname": "incoming_rate", + "fieldtype": "Float", + "label": "Incoming Rate", + }, + { + "fieldname": "outgoing_rate", + "fieldtype": "Float", + "label": "Outgoing Rate", + }, + { + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "label": "(A) Qty After Transaction", + }, + { + "fieldname": "expected_qty_after_transaction", + "fieldtype": "Float", + "label": "(B) Expected Qty After Transaction", + }, + { + "fieldname": "difference_in_qty", + "fieldtype": "Float", + "label": "A - B", + }, + { + "fieldname": "stock_queue", + "fieldtype": "Data", + "label": "FIFO Queue", + }, + + { + "fieldname": "fifo_queue_qty", + "fieldtype": "Float", + "label": "(C) Total qty in queue", + }, + { + "fieldname": "fifo_qty_diff", + "fieldtype": "Float", + "label": "A - C", + }, + { + "fieldname": "stock_value", + "fieldtype": "Float", + "label": "(D) Balance Stock Value", + }, + { + "fieldname": "fifo_stock_value", + "fieldtype": "Float", + "label": "(E) Balance Stock Value in Queue", + }, + { + "fieldname": "fifo_value_diff", + "fieldtype": "Float", + "label": "D - E", + }, + + { + "fieldname": "stock_value_difference", + "fieldtype": "Float", + "label": "(F) Stock Value Difference", + }, + { + "fieldname": "fifo_stock_diff", + "fieldtype": "Float", + "label": "(G) Stock Value difference (FIFO queue)", + }, + { + "fieldname": "fifo_difference_diff", + "fieldtype": "Float", + "label": "F - G", + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Float", + "label": "(H) Valuation Rate", + }, + { + "fieldname": "fifo_valuation_rate", + "fieldtype": "Float", + "label": "(I) Valuation Rate as per FIFO", + }, + + { + "fieldname": "fifo_valuation_diff", + "fieldtype": "Float", + "label": "H - I", + }, + { + "fieldname": "balance_value_by_qty", + "fieldtype": "Float", + "label": "(J) Valuation = Value (D) รท Qty (A)", + }, + { + "fieldname": "valuation_diff", + "fieldtype": "Float", + "label": "H - J", + }, + ] diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index d7fb5b2bf3f..1dcf863a9d0 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -41,6 +41,12 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("Total Stock Summary", {"group_by": "warehouse",}), ("Batch Item Expiry Status", {}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), + ("Stock Ledger Invariant Check", + { + "warehouse": "_Test Warehouse - _TC", + "item": "_Test Item" + } + ), ] OPTIONAL_FILTERS = { diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 91df5480e35..fbf25948a79 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import copy +import signal import unittest from contextlib import contextmanager from typing import Any, Dict, NewType, Optional @@ -135,3 +136,23 @@ def execute_script_report( report_execute_fn(filter_with_optional_param) return report_data + + +def timeout(seconds=30, error_message="Test timed out."): + """ Timeout decorator to ensure a test doesn't run for too long. + + adapted from https://stackoverflow.com/a/2282656""" + def decorator(func): + def _handle_timeout(signum, frame): + raise Exception(error_message) + + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.alarm(seconds) + try: + result = func(*args, **kwargs) + finally: + signal.alarm(0) + return result + return wrapper + return decorator