Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into rcm_tax_template_fetch_issue

This commit is contained in:
Deepesh Garg
2021-12-17 16:03:22 +05:30
46 changed files with 777 additions and 510 deletions

View File

@@ -295,8 +295,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): 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) 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: elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate: elif not item.expense_account and for_validate:

View File

@@ -24,6 +24,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account_currency
class Subscription(Document): class Subscription(Document):
@@ -356,6 +357,9 @@ class Subscription(Document):
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'): if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
invoice.apply_tds = 1 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: ## Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()

View File

@@ -60,15 +60,38 @@ def create_plan():
plan.billing_interval_count = 3 plan.billing_interval_count = 3
plan.insert() 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'): if not frappe.db.exists('Supplier', '_Test Supplier'):
supplier = frappe.new_doc('Supplier') supplier = frappe.new_doc('Supplier')
supplier.supplier_name = '_Test Supplier' supplier.supplier_name = '_Test Supplier'
supplier.supplier_group = 'All Supplier Groups' supplier.supplier_group = 'All Supplier Groups'
supplier.insert() 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): class TestSubscription(unittest.TestCase):
def setUp(self): def setUp(self):
create_plan() create_plan()
create_parties()
def test_create_subscription_with_trial_with_correct_period(self): def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription') subscription = frappe.new_doc('Subscription')
@@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase):
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 1) 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')

View File

@@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row): def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date": 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": elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date entry_date = row.bill_date
else: else:

View File

@@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date posting_date = entry.posting_date
voucher_type = entry.voucher_type 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: if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit) tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted: if tds_deducted:
row = { row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name') '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(): def get_supplier_pan_map():
supplier_map = frappe._dict() 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: for d in suppliers:
supplier_map[d.name] = d supplier_map[d.name] = d

View File

@@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
if (frm.doc.status != 'Fully Depreciated') { if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() { frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment"); frm.trigger("create_asset_value_adjustment");
}, __("Manage")); }, __("Manage"));
} }
@@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
}); });
}, },
create_asset_adjustment: function(frm) { create_asset_value_adjustment: function(frm) {
frappe.call({ frappe.call({
args: { args: {
"asset": frm.doc.name, "asset": frm.doc.name,
"asset_category": frm.doc.asset_category, "asset_category": frm.doc.asset_category,
"company": frm.doc.company "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, freeze: 1,
callback: function(r) { callback: function(r) {
var doclist = frappe.model.sync(r.message); var doclist = frappe.model.sync(r.message);

View File

@@ -470,7 +470,6 @@ class Asset(AccountsController):
asset_value_after_full_schedule = flt( asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) - flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation) -
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
if (row.expected_value_after_useful_life and if (row.expected_value_after_useful_life and
@@ -732,14 +731,14 @@ def create_asset_repair(asset, asset_name):
return asset_repair return asset_repair
@frappe.whitelist() @frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company): def create_asset_value_adjustment(asset, asset_category, company):
asset_maintenance = frappe.get_doc("Asset Value Adjustment") asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_maintenance.update({ asset_value_adjustment.update({
"asset": asset, "asset": asset,
"company": company, "company": company,
"asset_category": asset_category "asset_category": asset_category
}) })
return asset_maintenance return asset_value_adjustment
@frappe.whitelist() @frappe.whitelist()
def transfer_asset(args): def transfer_asset(args):

View File

@@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show() 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) => { preview: (frm) => {
let dialog = new frappe.ui.Dialog({ let dialog = new frappe.ui.Dialog({
title: __('Preview Email'), title: __('Preview Email'),
@@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show(); 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",{ frappe.ui.form.on("Request for Quotation Supplier",{
supplier: function(frm, cdt, cdn) { supplier: function(frm, cdt, cdn) {
var d = locals[cdt][cdn] var d = locals[cdt][cdn]

View File

@@ -12,6 +12,7 @@
"vendor", "vendor",
"column_break1", "column_break1",
"transaction_date", "transaction_date",
"schedule_date",
"status", "status",
"amended_from", "amended_from",
"suppliers_section", "suppliers_section",
@@ -246,16 +247,22 @@
"fieldname": "sec_break_email_2", "fieldname": "sec_break_email_2",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "hide_border": 1
},
{
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required Date"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-05 22:04:29.017134", "modified": "2021-11-24 17:47:49.909000",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -115,8 +115,7 @@
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f", "modified": "2021-11-30 12:17:24.647979",
"modified": "2021-11-30 11:17:24.647979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "TaxJar Settings", "name": "TaxJar Settings",

View File

@@ -56,9 +56,14 @@ class TestMaintenanceSchedule(unittest.TestCase):
ms.submit() ms.submit()
s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1]) 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 = frappe.new_doc('Maintenance Visit')
visit = test visit = test_map_visit
visit.maintenance_schedule = ms.name visit.maintenance_schedule = ms.name
visit.maintenance_schedule_detail = s_id visit.maintenance_schedule_detail = s_id
visit.completion_status = "Partially Completed" visit.completion_status = "Partially Completed"

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on('Maintenance Visit', {
frm.set_value({ status: 'Draft' }); frm.set_value({ status: 'Draft' });
} }
if (frm.doc.__islocal) { 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() }); frm.set_value({ mntc_date: frappe.datetime.get_today() });
} }
}, },

View File

@@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.utils import add_months, today from frappe.utils import add_months, today
from erpnext import get_company_currency from erpnext import get_company_currency
from erpnext.tests.utils import ERPNextTestCase
from .blanket_order import make_order from .blanket_order import make_order
class TestBlanketOrder(unittest.TestCase): class TestBlanketOrder(ERPNextTestCase):
def setUp(self): def setUp(self):
frappe.flags.args = frappe._dict() frappe.flags.args = frappe._dict()

View File

@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
from collections import deque from collections import deque
from functools import partial from functools import partial
@@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.tests.test_subcontracting import set_backflush_based_on from erpnext.tests.test_subcontracting import set_backflush_based_on
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
class TestBOM(unittest.TestCase): class TestBOM(ERPNextTestCase):
def setUp(self): def setUp(self):
if not frappe.get_value('Item', '_Test Item'): if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item') make_test_records('Item')

View File

@@ -1,19 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost 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.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
class TestBOMUpdateTool(unittest.TestCase): class TestBOMUpdateTool(ERPNextTestCase):
def test_replace_bom(self): def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001" current_bom = "BOM-_Test Item Home Desktop Manufactured-001"

View File

@@ -76,6 +76,15 @@ frappe.ui.form.on('Job Card', {
frm.trigger("prepare_timer_buttons"); frm.trigger("prepare_timer_buttons");
} }
frm.trigger("setup_quality_inspection"); 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) { setup_quality_inspection: function(frm) {

View File

@@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.utils import random_string 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.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation 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.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): def setUp(self):
make_bom_for_jc_tests() make_bom_for_jc_tests()
@@ -329,4 +329,4 @@ def make_bom_for_jc_tests():
bom.rm_cost_as_per = "Valuation Rate" bom.rm_cost_as_per = "Valuation Rate"
bom.items[0].uom = "_Test UOM 1" bom.items[0].uom = "_Test UOM 1"
bom.items[0].conversion_factor = 5 bom.items[0].conversion_factor = 5
bom.insert() bom.insert()

View File

@@ -1,8 +1,5 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.utils import add_to_date, flt, now_datetime, nowdate 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 ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestProductionPlan(unittest.TestCase): class TestProductionPlan(ERPNextTestCase):
def setUp(self): def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1', for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']: 'Raw Material Item 1', 'Raw Material Item 2']:

View File

@@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError 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.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.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
class TestRouting(unittest.TestCase): class TestRouting(ERPNextTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.item_code = "Test Routing Item - A" cls.item_code = "Test Routing Item - A"

View File

@@ -1,6 +1,5 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.utils import add_months, cint, flt, now, today 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.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.tests.utils import ERPNextTestCase, timeout
class TestWorkOrder(unittest.TestCase): class TestWorkOrder(ERPNextTestCase):
def setUp(self): def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC' self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item' self.item = '_Test Item'
@@ -91,7 +91,7 @@ class TestWorkOrder(unittest.TestCase):
def test_reserved_qty_for_partial_completion(self): def test_reserved_qty_for_partial_completion(self):
item = "_Test Item" item = "_Test Item"
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC") warehouse = "_Test Warehouse - _TC"
bin1_at_start = get_bin(item, warehouse) bin1_at_start = get_bin(item, warehouse)
@@ -376,6 +376,7 @@ class TestWorkOrder(unittest.TestCase):
self.assertEqual(len(ste.additional_costs), 1) self.assertEqual(len(ste.additional_costs), 1)
self.assertEqual(ste.total_additional_costs, 1000) self.assertEqual(ste.total_additional_costs, 1000)
@timeout(seconds=60)
def test_job_card(self): def test_job_card(self):
stock_entries = [] stock_entries = []
bom = frappe.get_doc('BOM', { bom = frappe.get_doc('BOM', {
@@ -769,6 +770,7 @@ class TestWorkOrder(unittest.TestCase):
total_pl_qty total_pl_qty
) )
@timeout(seconds=60)
def test_job_card_scrap_item(self): def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test'] 'Test RM Item 2 for Scrap Item Test']

View File

@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
@@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
WorkstationHolidayError, WorkstationHolidayError,
check_if_within_operating_hours, check_if_within_operating_hours,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Warehouse"] test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation') test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation') make_test_records('Workstation')
class TestWorkstation(unittest.TestCase): class TestWorkstation(ERPNextTestCase):
def test_validate_timings(self): 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 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") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")

View File

@@ -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.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse 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.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.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.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry 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") 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.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost 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.create_website_items #30-09-2021
erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items 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.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.update_tax_category_for_rcm

View File

@@ -3,10 +3,13 @@
import frappe import frappe
from erpnext.regional.saudi_arabia.setup import add_print_formats
def execute(): def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if company: if company:
add_print_formats()
return return
if frappe.db.exists('DocType', 'Print Format'): if frappe.db.exists('DocType', 'Print Format'):

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.utils.rename_field import rename_field from frappe.model.utils.rename_field import rename_field
@@ -12,5 +13,20 @@ def execute():
if frappe.db.exists('DocType', 'Sales Invoice'): if frappe.db.exists('DocType', 'Sales Invoice'):
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) 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'): if frappe.db.has_column('Sales Invoice', 'qr_code'):
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")

View File

@@ -944,10 +944,12 @@ class SalarySlip(TransactionBase):
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount 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 if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days) 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 (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 getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date) (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) amount = flt((flt(row.default_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount / 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 amount, additional_amount = 0, 0
elif not row.amount: elif not row.amount:
amount = flt(row.default_amount) + flt(row.additional_amount) amount = flt(row.default_amount) + flt(row.additional_amount)

View File

@@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") 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): def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import (

View File

@@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]: for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
frappe.db.sql("delete from `tab%s`" % dt) 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): def test_timesheet_billing_amount(self):
emp = make_employee("test_employee_6@salary.com") 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" salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly" 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 = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component" salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1 salary_structure.salary_slip_based_on_timesheet = 1

View File

@@ -115,9 +115,11 @@ def get_items(filters):
items = frappe.db.sql(""" items = frappe.db.sql("""
select select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, `tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount, 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, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description `tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` 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`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s 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) """ % (conditions, match_conditions), filters, as_dict=1)

View File

@@ -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)

View File

@@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.utils import flt from frappe.utils import flt
@@ -11,7 +9,7 @@ from frappe.utils import flt
from erpnext.accounts.party import get_due_date from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding 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_ignore = ["Price List"]
test_dependencies = ['Payment Term', 'Payment Terms Template'] test_dependencies = ['Payment Term', 'Payment Terms Template']
@@ -20,7 +18,7 @@ test_records = frappe.get_test_records('Customer')
from six import iteritems from six import iteritems
class TestCustomer(unittest.TestCase): class TestCustomer(ERPNextTestCase):
def setUp(self): def setUp(self):
if not frappe.get_value('Item', '_Test Item'): if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item') make_test_records('Item')

View File

@@ -6,6 +6,7 @@ import unittest
import frappe import frappe
from erpnext.controllers.queries import item_query from erpnext.controllers.queries import item_query
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ['Item', 'Customer', 'Supplier'] 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.based_on_value = args.get('based_on_value')
psi.insert() psi.insert()
class TestPartySpecificItem(unittest.TestCase): class TestPartySpecificItem(ERPNextTestCase):
def setUp(self): def setUp(self):
self.customer = frappe.get_last_doc("Customer") self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier") self.supplier = frappe.get_last_doc("Supplier")

View File

@@ -1,15 +1,15 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.utils import add_days, add_months, flt, getdate, nowdate from frappe.utils import add_days, add_months, flt, getdate, nowdate
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Product Bundle"] test_dependencies = ["Product Bundle"]
class TestQuotation(unittest.TestCase): class TestQuotation(ERPNextTestCase):
def test_make_quotation_without_terms(self): def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1) quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get('payment_schedule')) self.assertFalse(quotation.get('payment_schedule'))

View File

@@ -2,7 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import json import json
import unittest
import frappe import frappe
import frappe.permissions 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.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass()
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order")) "unlink_advance_payment_on_cancelation_of_order"))
@@ -36,6 +37,7 @@ class TestSalesOrder(unittest.TestCase):
# reset config to previous state # reset config to previous state
frappe.db.set_value("Accounts Settings", "Accounts Settings", frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
super().tearDownClass()
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@@ -2,8 +2,6 @@
# For license information, please see license.txt # For license information, please see license.txt
import unittest
from frappe.utils import add_months, nowdate from frappe.utils import add_months, nowdate
from erpnext.selling.doctype.sales_order.sales_order import make_material_request 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 ( from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
execute, execute,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestPendingSOItemsForPurchaseRequest(unittest.TestCase): class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
def test_result_for_partial_material_request(self): def test_result_for_partial_material_request(self):
so = make_sales_order() so = make_sales_order()
mr=make_material_request(so.name) mr=make_material_request(so.name)

View File

@@ -2,15 +2,14 @@
# For license information, please see license.txt # For license information, please see license.txt
import unittest
import frappe import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order 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.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): def test_sales_analytics(self):
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")

View File

@@ -33,10 +33,10 @@ class Bin(Document):
in open work orders''' in open work orders'''
self.reserved_qty_for_production = frappe.db.sql(''' self.reserved_qty_for_production = frappe.db.sql('''
SELECT SELECT
CASE WHEN ifnull(skip_transfer, 0) = 0 THEN SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN
SUM(item.required_qty - item.transferred_qty) item.required_qty - item.transferred_qty
ELSE ELSE
SUM(item.required_qty - item.consumed_qty) item.required_qty - item.consumed_qty END)
END END
FROM `tabWork Order` pro, `tabWork Order Item` item FROM `tabWork Order` pro, `tabWork Order Item` item
WHERE WHERE

View File

@@ -359,8 +359,7 @@
"fieldname": "valuation_method", "fieldname": "valuation_method",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Valuation Method", "label": "Valuation Method",
"options": "\nFIFO\nMoving Average", "options": "\nFIFO\nMoving Average"
"set_only_once": 1
}, },
{ {
"depends_on": "is_stock_item", "depends_on": "is_stock_item",
@@ -956,7 +955,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-12-03 08:32:03.869294", "modified": "2021-12-14 04:13:16.857534",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -1,451 +1,140 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0, "autoname": "hash",
"allow_rename": 0, "creation": "2013-04-08 13:10:16",
"autoname": "hash", "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2013-04-08 13:10:16", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "item_code",
"document_type": "Document", "column_break_2",
"editable_grid": 1, "item_name",
"engine": "InnoDB", "batch_no",
"desc_section",
"description",
"quantity_section",
"qty",
"net_weight",
"column_break_10",
"stock_uom",
"weight_uom",
"page_break",
"dn_detail"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "item_code",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_global_search": 1,
"columns": 0, "in_list_view": 1,
"fieldname": "item_code", "label": "Item Code",
"fieldtype": "Link", "options": "Item",
"hidden": 0, "print_width": "100px",
"ignore_user_permissions": 0, "reqd": 1,
"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,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "column_break_2",
"bold": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_on_submit": 0, "fetch_from": "item_code.item_name",
"bold": 0, "fieldname": "item_name",
"collapsible": 0, "fieldtype": "Data",
"columns": 0, "in_list_view": 1,
"fieldname": "item_name", "label": "Item Name",
"fieldtype": "Data", "print_width": "200px",
"hidden": 0, "read_only": 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": "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,
"width": "200px" "width": "200px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "batch_no",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "label": "Batch No",
"columns": 0, "options": "Batch"
"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
},
{ {
"allow_on_submit": 0, "collapsible": 1,
"bold": 0, "fieldname": "desc_section",
"collapsible": 1, "fieldtype": "Section Break",
"columns": 0, "label": "Description"
"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
},
{ {
"allow_on_submit": 0, "fieldname": "description",
"bold": 0, "fieldtype": "Text Editor",
"collapsible": 0, "label": "Description"
"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
},
{ {
"allow_on_submit": 0, "fieldname": "quantity_section",
"bold": 0, "fieldtype": "Section Break",
"collapsible": 0, "label": "Quantity"
"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
},
{ {
"allow_on_submit": 0, "fieldname": "qty",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Quantity",
"fieldname": "qty", "print_width": "100px",
"fieldtype": "Float", "reqd": 1,
"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,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "net_weight",
"bold": 0, "fieldtype": "Float",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Net Weight",
"fieldname": "net_weight", "print_width": "100px",
"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,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "column_break_10",
"bold": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_on_submit": 0, "fieldname": "stock_uom",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "label": "UOM",
"columns": 0, "options": "UOM",
"fieldname": "stock_uom", "print_width": "100px",
"fieldtype": "Link", "read_only": 1,
"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,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "weight_uom",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "label": "Weight UOM",
"columns": 0, "options": "UOM",
"fieldname": "weight_uom", "print_width": "100px",
"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,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0, "default": "0",
"collapsible": 0, "fieldname": "page_break",
"columns": 0, "fieldtype": "Check",
"fieldname": "page_break", "in_list_view": 1,
"fieldtype": "Check", "label": "Page Break"
"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": 0, "fieldname": "dn_detail",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "hidden": 1,
"columns": 0, "in_list_view": 1,
"fieldname": "dn_detail", "label": "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
} }
], ],
"hide_heading": 0, "idx": 1,
"hide_toolbar": 0, "istable": 1,
"idx": 1, "links": [],
"image_view": 0, "modified": "2021-12-14 01:22:00.715935",
"in_create": 0, "modified_by": "Administrator",
"module": "Stock",
"is_submittable": 0, "name": "Packing Slip Item",
"issingle": 0, "naming_rule": "Random",
"istable": 1, "owner": "Administrator",
"max_attachments": 0, "permissions": [],
"modified": "2018-06-01 07:21:58.220980", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Stock", "track_changes": 1
"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
} }

View File

@@ -48,6 +48,7 @@ def get_item_info(filters):
conditions = [get_item_group_condition(filters.get("item_group"))] conditions = [get_item_group_condition(filters.get("item_group"))]
if filters.get("brand"): if filters.get("brand"):
conditions.append("item.brand=%(brand)s") conditions.append("item.brand=%(brand)s")
conditions.append("is_stock_item = 1")
return frappe.db.sql("""select name, item_name, description, brand, item_group, return frappe.db.sql("""select name, item_name, description, brand, item_group,
safety_stock, lead_time_days from `tabItem` item where {}""" safety_stock, lead_time_days from `tabItem` item where {}"""

View File

@@ -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.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 sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
from from
`tabStock Ledger Entry` sle force index (posting_sort_index) `tabStock Ledger Entry` sle
where sle.docstatus < 2 %s %s where sle.docstatus < 2 %s %s
and is_cancelled = 0 and is_cancelled = 0
order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec

View File

@@ -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 = "<span style='color:red'>" + value + "</span>";
}
return value;
},
};

View File

@@ -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"
}
]
}

View File

@@ -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",
},
]

View File

@@ -41,6 +41,12 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Total Stock Summary", {"group_by": "warehouse",}), ("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}), ("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ledger Invariant Check",
{
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item"
}
),
] ]
OPTIONAL_FILTERS = { OPTIONAL_FILTERS = {

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy import copy
import signal
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional from typing import Any, Dict, NewType, Optional
@@ -135,3 +136,23 @@ def execute_script_report(
report_execute_fn(filter_with_optional_param) report_execute_fn(filter_with_optional_param)
return report_data 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