Merge pull request #28745 from noahjacob/maint_batch_map
refactor: map serial from schedule if only one
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 5cbf00b..e7371fb 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -434,7 +434,7 @@
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
- mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
+ mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
filters={"default_account": bank_account}, fields=["parent"])]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
company_currency = get_company_currency(company)
diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
index bc8c6ab..69e13a4 100644
--- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
@@ -5,10 +5,10 @@
import unittest
import frappe
+from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
-test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
@@ -25,3 +25,29 @@
})
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
+
+
+def test_record_generator():
+ test_records = [
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Short Fiscal Year 2011",
+ "is_short_year": 1,
+ "year_end_date": "2011-04-01",
+ "year_start_date": "2011-12-31"
+ }
+ ]
+
+ start = 2012
+ end = now_datetime().year + 5
+ for year in range(start, end):
+ test_records.append({
+ "doctype": "Fiscal Year",
+ "year": f"_Test Fiscal Year {year}",
+ "year_start_date": f"{year}-01-01",
+ "year_end_date": f"{year}-12-31"
+ })
+
+ return test_records
+
+test_records = test_record_generator()
diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json
deleted file mode 100644
index 4405253..0000000
--- a/erpnext/accounts/doctype/fiscal_year/test_records.json
+++ /dev/null
@@ -1,69 +0,0 @@
-[
- {
- "doctype": "Fiscal Year",
- "year": "_Test Short Fiscal Year 2011",
- "is_short_year": 1,
- "year_end_date": "2011-04-01",
- "year_start_date": "2011-12-31"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2012",
- "year_end_date": "2012-12-31",
- "year_start_date": "2012-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2013",
- "year_end_date": "2013-12-31",
- "year_start_date": "2013-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2014",
- "year_end_date": "2014-12-31",
- "year_start_date": "2014-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2015",
- "year_end_date": "2015-12-31",
- "year_start_date": "2015-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2016",
- "year_end_date": "2016-12-31",
- "year_start_date": "2016-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2017",
- "year_end_date": "2017-12-31",
- "year_start_date": "2017-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2018",
- "year_end_date": "2018-12-31",
- "year_start_date": "2018-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2019",
- "year_end_date": "2019-12-31",
- "year_start_date": "2019-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2020",
- "year_end_date": "2020-12-31",
- "year_start_date": "2020-01-01"
- },
- {
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2021",
- "year_end_date": "2021-12-31",
- "year_start_date": "2021-01-01"
- }
-]
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index bff8587..0c6e7ed 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -171,6 +171,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -1561,16 +1562,23 @@
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-27 20:12:57.306772",
+ "modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 8b71eb0..3f85668 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -46,6 +46,7 @@
"base_amount",
"pricing_rules",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -800,14 +801,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-01-04 17:34:49.924531",
+ "modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index efdeb1a..813d20d 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -3,22 +3,20 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
-frappe.ui.form.on("POS Profile", "onload", function(frm) {
- frm.set_query("selling_price_list", function() {
- return { filters: { selling: 1 } };
- });
-
- frm.set_query("tc_name", function() {
- return { filters: { selling: 1 } };
- });
-
- erpnext.queries.setup_queries(frm, "Warehouse", function() {
- return erpnext.queries.warehouse(frm.doc);
- });
-});
-
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
+ frm.set_query("selling_price_list", function() {
+ return { filters: { selling: 1 } };
+ });
+
+ frm.set_query("tc_name", function() {
+ return { filters: { selling: 1 } };
+ });
+
+ erpnext.queries.setup_queries(frm, "Warehouse", function() {
+ return erpnext.queries.warehouse(frm.doc);
+ });
+
frm.set_query("print_format", function() {
return {
filters: [
@@ -27,10 +25,16 @@
};
});
- frm.set_query("account_for_change_amount", function() {
+ frm.set_query("account_for_change_amount", function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
return {
filters: {
- account_type: ['in', ["Cash", "Bank"]]
+ account_type: ['in', ["Cash", "Bank"]],
+ is_group: 0,
+ company: doc.company
}
};
});
@@ -45,7 +49,7 @@
});
frm.set_query('company_address', function(doc) {
- if(!doc.company) {
+ if (!doc.company) {
frappe.throw(__('Please set Company'));
}
@@ -58,11 +62,79 @@
};
});
+ frm.set_query('income_account', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company,
+ 'account_type': "Income Account"
+ }
+ };
+ });
+
+ frm.set_query('cost_center', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ 'company': doc.company,
+ 'is_group': 0
+ }
+ };
+ });
+
+ frm.set_query('expense_account', function(doc) {
+ if (!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ filters: {
+ "report_type": "Profit and Loss",
+ "company": doc.company,
+ "is_group": 0
+ }
+ };
+ });
+
+ frm.set_query("select_print_heading", function() {
+ return {
+ filters: [
+ ['Print Heading', 'docstatus', '!=', 2]
+ ]
+ };
+ });
+
+ frm.set_query("write_off_account", function(doc) {
+ return {
+ filters: {
+ 'report_type': 'Profit and Loss',
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ });
+
+ frm.set_query("write_off_cost_center", function(doc) {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'company': doc.company
+ }
+ };
+ });
+
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
- if(frm.doc.company) {
+ if (frm.doc.company) {
frm.trigger("toggle_display_account_head");
}
},
@@ -76,71 +148,4 @@
frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company));
}
-})
-
-// Income Account
-// --------------------------------
-cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
- return{
- filters:{
- 'is_group': 0,
- 'company': doc.company,
- 'account_type': "Income Account"
- }
- };
-};
-
-
-// Cost Center
-// -----------------------------
-cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
- return{
- filters:{
- 'company': doc.company,
- 'is_group': 0
- }
- };
-};
-
-
-// Expense Account
-// -----------------------------
-cur_frm.fields_dict["expense_account"].get_query = function(doc) {
- return {
- filters: {
- "report_type": "Profit and Loss",
- "company": doc.company,
- "is_group": 0
- }
- };
-};
-
-// ------------------ Get Print Heading ------------------------------------
-cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
- return{
- filters:[
- ['Print Heading', 'docstatus', '!=', 2]
- ]
- };
-};
-
-cur_frm.fields_dict.write_off_account.get_query = function(doc) {
- return{
- filters:{
- 'report_type': 'Profit and Loss',
- 'is_group': 0,
- 'company': doc.company
- }
- };
-};
-
-// Write off cost center
-// -----------------------
-cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
- return{
- filters:{
- 'is_group': 0,
- 'company': doc.company
- }
- };
-};
+});
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 4538675..39dfd8d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -516,15 +516,6 @@
}
}
-// project name
-//--------------------------
-cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
- return{
- query: "erpnext.controllers.queries.get_project_name",
- filters: {'customer': doc.customer}
- }
-}
-
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 93e32f1..545abf7 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -182,6 +182,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@@ -2019,6 +2020,12 @@
"label": "Total Billing Hours",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-10-11 20:19:38.667508",
+ "modified": "2021-10-21 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2086,4 +2093,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 37bea70..6a488ea 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2385,6 +2385,29 @@
si.reload()
self.assertEqual(si.status, "Paid")
+ def test_sales_commission(self):
+ si = frappe.copy_doc(test_records[0])
+ item = copy.deepcopy(si.get('items')[0])
+ item.update({
+ "qty": 1,
+ "rate": 500,
+ "grant_commission": 1
+ })
+ si.append("items", item)
+
+ # Test valid values
+ for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
+ si.commission_rate = commission_rate
+ si.save()
+ self.assertEqual(si.amount_eligible_for_commission, 500)
+ self.assertEqual(si.total_commission, total_commission)
+
+ # Test invalid values
+ for commission_rate in (101, -1):
+ si.reload()
+ si.commission_rate = commission_rate
+ self.assertRaises(frappe.ValidationError, si.save)
+
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index b90f3f0..ae9ac35 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -47,6 +47,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@@ -828,15 +829,23 @@
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-19 13:41:53.435827",
+ "modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e904768..6b4b43d 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -68,10 +68,12 @@
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address)
- if not party_details.get("taxes_and_charges"):
- party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
- customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
- billing_address=party_address, shipping_address=shipping_address)
+ tax_template = set_taxes(party.name, party_type, posting_date, company,
+ customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
+ billing_address=party_address, shipping_address=shipping_address)
+
+ if tax_template:
+ party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 88bcdad..353f908 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -109,7 +109,11 @@
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
- outstanding = 0.0
+ outstanding = 0.0,
+ invoiced_in_account_currency = 0.0,
+ paid_in_account_currency = 0.0,
+ credit_note_in_account_currency = 0.0,
+ outstanding_in_account_currency = 0.0
)
self.get_invoices(gle)
@@ -150,21 +154,28 @@
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
+ gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
+
if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice
row.paid -= gle_balance
+ row.paid_in_account_currency -= gle_balance_in_account_currency
else:
# invoice
row.invoiced += gle_balance
+ row.invoiced_in_account_currency += gle_balance_in_account_currency
else:
# payment or credit note for receivables
if self.is_invoice(gle):
# stand alone debit / credit note
row.credit_note -= gle_balance
+ row.credit_note_in_account_currency -= gle_balance_in_account_currency
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
+ row.paid_in_account_currency -= gle_balance_in_account_currency
+
if gle.cost_center:
row.cost_center = str(gle.cost_center)
@@ -216,8 +227,13 @@
# as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
+ row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
+ row.credit_note_in_account_currency, self.currency_precision)
+
row.invoice_grand_total = row.invoiced
- if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
+
+ if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
+ (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@@ -583,12 +599,14 @@
else:
select_fields = "debit, credit"
+ doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
+
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
- against_voucher_type, against_voucher, account_currency, {0} {remarks}
+ against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from
`tabGL Entry`
where
@@ -596,8 +614,8 @@
and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
- {1} {2} {3}"""
- .format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
+ {2} {3} {4}"""
+ .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -718,6 +736,13 @@
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
+ def get_gle_balance_in_account_currency(self, gle):
+ # get the balance of the GL (debit - credit) or reverse balance based on report type
+ return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
+
+ def get_reverse_balance_in_account_currency(self, gle):
+ return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
+
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index e24a5f9..d3e836a 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -92,6 +92,11 @@
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
+ },
+ {
+ "fieldname": "show_zero_values",
+ "label": __("Show zero values"),
+ "fieldtype": "Check"
}
],
"formatter": function(value, row, column, data, default_formatter) {
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index c71bc17..01799d5 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -22,7 +22,11 @@
get_cash_flow_accounts,
)
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
-from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts
+from erpnext.accounts.report.financial_statements import (
+ filter_out_zero_value_rows,
+ get_fiscal_year_data,
+ sort_accounts,
+)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data,
)
@@ -265,7 +269,7 @@
return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
- accounts, accounts_by_name = get_account_heads(root_type,
+ accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters)
if not accounts: return []
@@ -294,6 +298,8 @@
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
+ out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
+
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -370,7 +376,7 @@
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
- return accounts, accounts_by_name
+ return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
diff --git a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
index a740de3..9dd4e43 100644
--- a/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
+++ b/erpnext/accounts/report/sales_partners_commission/sales_partners_commission.json
@@ -1,27 +1,30 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2013-05-06 12:28:23",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 3,
- "is_standard": "Yes",
- "modified": "2017-03-06 05:52:57.645281",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Partners Commission",
- "owner": "Administrator",
- "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
- "ref_doctype": "Sales Invoice",
- "report_name": "Sales Partners Commission",
- "report_type": "Query Report",
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2013-05-06 12:28:23",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 3,
+ "is_standard": "Yes",
+ "modified": "2021-10-06 06:26:07.881340",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Partners Commission",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
+ "ref_doctype": "Sales Invoice",
+ "report_name": "Sales Partners Commission",
+ "report_type": "Query Report",
"roles": [
{
"role": "Accounts Manager"
- },
+ },
{
"role": "Accounts User"
}
]
-}
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index da5778e..c2b1bbc 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -80,20 +80,20 @@
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
- frm.add_custom_button("Transfer Asset", function() {
+ frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm);
}, __("Manage"));
- frm.add_custom_button("Scrap Asset", function() {
+ frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm);
}, __("Manage"));
- frm.add_custom_button("Sell Asset", function() {
+ frm.add_custom_button(__("Sell Asset"), function() {
frm.trigger("make_sales_invoice");
}, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
- frm.add_custom_button("Restore Asset", function() {
+ frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm);
}, __("Manage"));
}
@@ -121,7 +121,7 @@
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
- frm.add_custom_button("View General Ledger", function() {
+ frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index c0c437f..333906a 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -192,8 +192,7 @@
# value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation:
- value_after_depreciation = (flt(d.value_after_depreciation) -
- flt(self.opening_accumulated_depreciation))
+ value_after_depreciation = flt(d.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
@@ -241,7 +240,7 @@
break
# For first row
- if has_pro_rata and n==0:
+ if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
@@ -254,7 +253,7 @@
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
- n * cint(d.frequency_of_depreciation))
+ (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
@@ -354,7 +353,12 @@
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
- days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+ # if not existing asset, from_date = available_for_use_date
+ # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
+ # from_date = 01/01/2022
+ from_date = self.get_modified_available_for_use_date(row)
+ days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
@@ -364,6 +368,9 @@
return has_pro_rata
+ def get_modified_available_for_use_date(self, row):
+ return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
+
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@@ -402,10 +409,11 @@
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
- depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
+ if not self.opening_accumulated_depreciation:
+ depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
- if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
- depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
+ if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
+ depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
return depreciation_amount_for_last_row
@@ -850,13 +858,11 @@
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
- depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
-
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
- depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ depreciation_amount = (flt(asset.gross_purchase_amount) -
+ flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index ca10b1d..874fb63 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -57,8 +57,10 @@
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
+ credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
+
credit_entry = {
- "account": accumulated_depreciation_account,
+ "account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -66,7 +68,7 @@
}
debit_entry = {
- "account": depreciation_expense_account,
+ "account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@@ -132,6 +134,20 @@
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
+def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
+ root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
+
+ if root_type == "Expense":
+ credit_account = accumulated_depreciation_account
+ debit_account = depreciation_expense_account
+ elif root_type == "Income":
+ credit_account = depreciation_expense_account
+ debit_account = accumulated_depreciation_account
+ else:
+ frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
+
+ return credit_account, debit_account
+
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index d1d4527..ce2cb01 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -409,19 +409,18 @@
calculate_depreciation = 1,
available_for_use_date = "2030-06-06",
is_existing_asset = 1,
- number_of_depreciations_booked = 1,
- opening_accumulated_depreciation = 40000,
+ number_of_depreciations_booked = 2,
+ opening_accumulated_depreciation = 47095.89,
expected_value_after_useful_life = 10000,
- depreciation_start_date = "2030-12-31",
+ depreciation_start_date = "2032-12-31",
total_number_of_depreciations = 3,
frequency_of_depreciation = 12
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [
- ["2030-12-31", 14246.58, 54246.58],
- ["2031-12-31", 25000.00, 79246.58],
- ["2032-06-06", 10753.42, 90000.00]
+ ["2032-12-31", 30000.0, 77095.89],
+ ["2033-06-06", 12904.11, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
@@ -869,6 +868,72 @@
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry)
+ def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
+ """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+ else:
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+
+ def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
+ """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
+
+ depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+ depr_expense_account.root_type = "Income"
+ depr_expense_account.parent_account = "Income - _TC"
+ depr_expense_account.save()
+
+ asset = create_asset(
+ item_code = "Macbook Pro",
+ calculate_depreciation = 1,
+ available_for_use_date = "2019-12-31",
+ depreciation_start_date = "2020-12-31",
+ frequency_of_depreciation = 12,
+ total_number_of_depreciations = 3,
+ expected_value_after_useful_life = 10000,
+ submit = 1
+ )
+
+ post_depreciation_entries(date="2021-06-01")
+ asset.load_from_db()
+
+ je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
+ accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
+
+ for entry in accounting_entries:
+ if entry["account"] == "_Test Depreciations - _TC":
+ self.assertTrue(entry["credit"])
+ self.assertFalse(entry["debit"])
+ else:
+ self.assertTrue(entry["debit"])
+ self.assertFalse(entry["credit"])
+
+ # resetting
+ depr_expense_account.root_type = "Expense"
+ depr_expense_account.parent_account = "Expenses - _TC"
+ depr_expense_account.save()
+
def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected."""
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index 51ce157..c702687 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,7 +33,7 @@
var d = locals[cdt][cdn];
return {
"filters": {
- "root_type": "Expense",
+ "root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name
}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index e2f3ca3..bd573bf 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -42,10 +42,10 @@
def validate_account_types(self):
account_type_map = {
- 'fixed_asset_account': { 'account_type': 'Fixed Asset' },
- 'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
- 'depreciation_expense_account': { 'root_type': 'Expense' },
- 'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
+ 'fixed_asset_account': {'account_type': ['Fixed Asset']},
+ 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
+ 'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
+ 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
}
for d in self.accounts:
for fieldname in account_type_map.keys():
@@ -53,11 +53,11 @@
selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
- expected_key_type = account_type_map[fieldname][key_to_match]
+ expected_key_types = account_type_map[fieldname][key_to_match]
- if selected_key_type != expected_key_type:
+ if selected_key_type not in expected_key_types:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
- .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
+ .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account"))
def valide_cwip_account(self):
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 18a56d3..d554d52 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -60,6 +60,10 @@
if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime());
}
+ },
+
+ stock_items_on_form_rendered() {
+ erpnext.setup_serial_or_batch_no();
}
});
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index d780c18..36848e9 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -118,9 +118,10 @@
for stock_item in self.get('stock_items'):
stock_entry.append('items', {
"s_warehouse": self.warehouse,
- "item_code": stock_item.item,
+ "item_code": stock_item.item_code,
"qty": stock_item.consumed_quantity,
- "basic_rate": stock_item.valuation_rate
+ "basic_rate": stock_item.valuation_rate,
+ "serial_no": stock_item.serial_no
})
stock_entry.insert()
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 81b4f6c..7c0d057 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -11,12 +11,15 @@
create_asset_data,
set_depreciation_settings_in_company,
)
+from erpnext.stock.doctype.item.test_item import create_item
class TestAssetRepair(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
set_depreciation_settings_in_company()
create_asset_data()
+ create_item("_Test Stock Item")
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
@@ -70,9 +73,28 @@
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
- self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
+ self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+ def test_serialized_item_consumption(self):
+ from erpnext.stock.doctype.serial_no.serial_no import SerialNoRequiredError
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+
+ stock_entry = make_serialized_item()
+ serial_nos = stock_entry.get("items")[0].serial_no
+ serial_no = serial_nos.split("\n")[0]
+
+ # should not raise any error
+ create_asset_repair(stock_consumption = 1, item_code = stock_entry.get("items")[0].item_code,
+ warehouse = "_Test Warehouse - _TC", serial_no = serial_no, submit = 1)
+
+ # should raise error
+ asset_repair = create_asset_repair(stock_consumption = 1, warehouse = "_Test Warehouse - _TC",
+ item_code = stock_entry.get("items")[0].item_code)
+
+ asset_repair.repair_status = "Completed"
+ self.assertRaises(SerialNoRequiredError, asset_repair.submit)
+
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
@@ -137,11 +159,12 @@
if args.stock_consumption:
asset_repair.stock_consumption = 1
- asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+ asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", {
- "item": args.item or args.item_code or "_Test Item",
+ "item_code": args.item_code or "_Test Stock Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
- "consumed_quantity": args.qty or 1
+ "consumed_quantity": args.qty or 1,
+ "serial_no": args.serial_no
})
asset_repair.insert(ignore_if_duplicate=True)
@@ -158,7 +181,7 @@
})
stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse,
- "item_code": asset_repair.stock_items[0].item,
+ "item_code": asset_repair.stock_items[0].item_code,
"qty": asset_repair.stock_items[0].consumed_quantity
})
stock_entry.submit()
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
index 528f0ec..f63add1 100644
--- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -5,20 +5,14 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "item",
+ "item_code",
"valuation_rate",
"consumed_quantity",
- "total_value"
+ "total_value",
+ "serial_no"
],
"fields": [
{
- "fieldname": "item",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Item",
- "options": "Item"
- },
- {
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
@@ -38,12 +32,24 @@
"in_list_view": 1,
"label": "Total Value",
"read_only": 1
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-12 03:19:55.006300",
+ "modified": "2021-11-11 18:23:00.492483",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 0ee884e..2c92820 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -250,7 +250,12 @@
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
calculate_taxes_and_totals(self)
- if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
+ if self.doctype in (
+ 'Sales Order',
+ 'Delivery Note',
+ 'Sales Invoice',
+ 'POS Invoice',
+ ):
self.calculate_commission()
self.calculate_contribution()
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 76a7d1a..dc04dab 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -539,6 +539,10 @@
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
+ or_filters = []
+ fields = ['name']
+
+ searchfields = frappe.get_meta(doctype).get_search_fields()
meta = frappe.get_meta(doctype)
if meta.is_tree:
@@ -550,8 +554,9 @@
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
- if txt:
- query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
+ for field in searchfields:
+ or_filters.append([field, 'LIKE', "%%%s%%" % txt])
+ fields.append(field)
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
@@ -566,10 +571,9 @@
query_filters.append(['name', query_selector, dimensions])
- output = frappe.get_list(doctype, filters=query_filters)
- result = [d.name for d in output]
+ output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1)
- return [(d,) for d in set(result)]
+ return [tuple(d) for d in set(output)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index dad3ed7..cc773b7 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -120,13 +120,27 @@
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
- if self.meta.get_field("commission_rate"):
- self.round_floats_in(self, ["base_net_total", "commission_rate"])
- if self.commission_rate > 100.0:
- throw(_("Commission rate cannot be greater than 100"))
+ if not self.meta.get_field("commission_rate"):
+ return
- self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0,
- self.precision("total_commission"))
+ self.round_floats_in(
+ self, ("amount_eligible_for_commission", "commission_rate")
+ )
+
+ if not (0 <= self.commission_rate <= 100.0):
+ throw("{} {}".format(
+ _(self.meta.get_label("commission_rate")),
+ _("must be between 0 and 100"),
+ ))
+
+ self.amount_eligible_for_commission = sum(
+ item.base_net_amount for item in self.items if item.grant_commission
+ )
+
+ self.total_commission = flt(
+ self.amount_eligible_for_commission * self.commission_rate / 100.0,
+ self.precision("total_commission")
+ )
def calculate_contribution(self):
if not self.meta.get_field("sales_team"):
@@ -138,7 +152,7 @@
self.round_floats_in(sales_person)
sales_person.allocated_amount = flt(
- self.base_net_total * sales_person.allocated_percentage / 100.0,
+ self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
if sales_person.commission_rate:
diff --git a/erpnext/crm/doctype/crm_settings/__init__.py b/erpnext/crm/doctype/crm_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/__init__.py
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.js b/erpnext/crm/doctype/crm_settings/crm_settings.js
new file mode 100644
index 0000000..c6569d8
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('CRM Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json
new file mode 100644
index 0000000..95b19fa
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -0,0 +1,114 @@
+{
+ "actions": [],
+ "creation": "2021-09-09 17:03:22.754446",
+ "description": "Settings for Selling Module",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "engine": "InnoDB",
+ "field_order": [
+ "section_break_5",
+ "campaign_naming_by",
+ "allow_lead_duplication_based_on_emails",
+ "column_break_4",
+ "create_event_on_next_contact_date",
+ "auto_creation_of_contact",
+ "opportunity_section",
+ "close_opportunity_after_days",
+ "column_break_9",
+ "create_event_on_next_contact_date_opportunity",
+ "quotation_section",
+ "default_valid_till"
+ ],
+ "fields": [
+ {
+ "fieldname": "campaign_naming_by",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Campaign Naming By",
+ "options": "Campaign Name\nNaming Series"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_valid_till",
+ "fieldtype": "Data",
+ "label": "Default Quotation Validity Days"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Lead"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_lead_duplication_based_on_emails",
+ "fieldtype": "Check",
+ "label": "Allow Lead Duplication based on Emails"
+ },
+ {
+ "default": "1",
+ "fieldname": "auto_creation_of_contact",
+ "fieldtype": "Check",
+ "label": "Auto Creation of Contact"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "opportunity_section",
+ "fieldtype": "Section Break",
+ "label": "Opportunity"
+ },
+ {
+ "default": "15",
+ "description": "Auto close Opportunity Replied after the no. of days mentioned above",
+ "fieldname": "close_opportunity_after_days",
+ "fieldtype": "Int",
+ "label": "Close Replied Opportunity After Days"
+ },
+ {
+ "default": "1",
+ "fieldname": "create_event_on_next_contact_date_opportunity",
+ "fieldtype": "Check",
+ "label": "Create Event on Next Contact Date"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "quotation_section",
+ "fieldtype": "Section Break",
+ "label": "Quotation"
+ }
+ ],
+ "icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
+ "modified": "2021-11-03 09:00:36.883496",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.py b/erpnext/crm/doctype/crm_settings/crm_settings.py
new file mode 100644
index 0000000..bde5254
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CRMSettings(Document):
+ pass
diff --git a/erpnext/crm/doctype/crm_settings/test_crm_settings.py b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
new file mode 100644
index 0000000..3372c5d
--- /dev/null
+++ b/erpnext/crm/doctype/crm_settings/test_crm_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestCRMSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c590523..9adbe8b 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -11,6 +11,7 @@
cint,
comma_and,
cstr,
+ get_link_to_form,
getdate,
has_gravatar,
nowdate,
@@ -91,13 +92,14 @@
self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False):
- super(Lead, self).add_calendar_event({
- "owner": self.lead_owner,
- "starts_on": self.contact_date,
- "ends_on": self.ends_on or "",
- "subject": ('Contact ' + cstr(self.lead_name)),
- "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
- }, force)
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
+ super(Lead, self).add_calendar_event({
+ "owner": self.lead_owner,
+ "starts_on": self.contact_date,
+ "ends_on": self.ends_on or "",
+ "subject": ('Contact ' + cstr(self.lead_name)),
+ "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
+ }, force)
def update_prospects(self):
prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
@@ -108,12 +110,13 @@
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
- duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
- duplicate_leads = [lead.name for lead in duplicate_leads]
+ if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
+ duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
+ duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
- if duplicate_leads:
- frappe.throw(_("Email Address must be unique, already exists for {0}")
- .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
+ if duplicate_leads:
+ frappe.throw(_("Email Address must be unique, already exists for {0}")
+ .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
@@ -172,41 +175,42 @@
self.title = self.company_name or self.lead_name
def create_contact(self):
- if not self.lead_name:
- self.set_full_name()
- self.set_lead_name()
+ if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
+ if not self.lead_name:
+ self.set_full_name()
+ self.set_lead_name()
- contact = frappe.new_doc("Contact")
- contact.update({
- "first_name": self.first_name or self.lead_name,
- "last_name": self.last_name,
- "salutation": self.salutation,
- "gender": self.gender,
- "designation": self.designation,
- "company_name": self.company_name,
- })
-
- if self.email_id:
- contact.append("email_ids", {
- "email_id": self.email_id,
- "is_primary": 1
+ contact = frappe.new_doc("Contact")
+ contact.update({
+ "first_name": self.first_name or self.lead_name,
+ "last_name": self.last_name,
+ "salutation": self.salutation,
+ "gender": self.gender,
+ "designation": self.designation,
+ "company_name": self.company_name,
})
- if self.phone:
- contact.append("phone_nos", {
- "phone": self.phone,
- "is_primary_phone": 1
- })
+ if self.email_id:
+ contact.append("email_ids", {
+ "email_id": self.email_id,
+ "is_primary": 1
+ })
- if self.mobile_no:
- contact.append("phone_nos", {
- "phone": self.mobile_no,
- "is_primary_mobile_no":1
- })
+ if self.phone:
+ contact.append("phone_nos", {
+ "phone": self.phone,
+ "is_primary_phone": 1
+ })
- contact.insert(ignore_permissions=True)
+ if self.mobile_no:
+ contact.append("phone_nos", {
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
+ })
- return contact
+ contact.insert(ignore_permissions=True)
+
+ return contact
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 0bef80a..fcbd4de 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -8,6 +8,7 @@
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname
from erpnext.setup.utils import get_exchange_rate
@@ -28,7 +29,6 @@
})
self.make_new_lead_if_required()
-
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
@@ -70,21 +70,21 @@
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
# check if customer is already created agains the self.contact_email
- customer = frappe.db.sql("""select
- distinct `tabDynamic Link`.link_name as customer
- from
- `tabContact`,
- `tabDynamic Link`
- where `tabContact`.email_id='{0}'
- and
- `tabContact`.name=`tabDynamic Link`.parent
- and
- ifnull(`tabDynamic Link`.link_name, '')<>''
- and
- `tabDynamic Link`.link_doctype='Customer'
- """.format(self.contact_email), as_dict=True)
- if customer and customer[0].customer:
- self.party_name = customer[0].customer
+ dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
+ customer = frappe.qb.from_(
+ dynamic_link
+ ).join(
+ contact
+ ).on(
+ (contact.name == dynamic_link.parent)
+ & (dynamic_link.link_doctype == "Customer")
+ & (contact.email_id == self.contact_email)
+ ).select(
+ dynamic_link.link_name
+ ).distinct().run(as_dict=True)
+
+ if customer and customer[0].link_name:
+ self.party_name = customer[0].link_name
self.opportunity_from = "Customer"
return
@@ -191,30 +191,31 @@
self.add_calendar_event()
def add_calendar_event(self, opts=None, force=False):
- if not opts:
- opts = frappe._dict()
+ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
+ if not opts:
+ opts = frappe._dict()
- opts.description = ""
- opts.contact_date = self.contact_date
+ opts.description = ""
+ opts.contact_date = self.contact_date
- if self.party_name and self.opportunity_from == 'Customer':
- if self.contact_person:
- opts.description = 'Contact '+cstr(self.contact_person)
- else:
- opts.description = 'Contact customer '+cstr(self.party_name)
- elif self.party_name and self.opportunity_from == 'Lead':
- if self.contact_display:
- opts.description = 'Contact '+cstr(self.contact_display)
- else:
- opts.description = 'Contact lead '+cstr(self.party_name)
+ if self.party_name and self.opportunity_from == 'Customer':
+ if self.contact_person:
+ opts.description = 'Contact '+cstr(self.contact_person)
+ else:
+ opts.description = 'Contact customer '+cstr(self.party_name)
+ elif self.party_name and self.opportunity_from == 'Lead':
+ if self.contact_display:
+ opts.description = 'Contact '+cstr(self.contact_display)
+ else:
+ opts.description = 'Contact lead '+cstr(self.party_name)
- opts.subject = opts.description
- opts.description += '. By : ' + cstr(self.contact_by)
+ opts.subject = opts.description
+ opts.description += '. By : ' + cstr(self.contact_by)
- if self.to_discuss:
- opts.description += ' To Discuss : ' + cstr(self.to_discuss)
+ if self.to_discuss:
+ opts.description += ' To Discuss : ' + cstr(self.to_discuss)
- super(Opportunity, self).add_calendar_event(opts, force)
+ super(Opportunity, self).add_calendar_event(opts, force)
def validate_item_details(self):
if not self.get('items'):
@@ -363,7 +364,7 @@
def auto_close_opportunity():
""" auto close the `Replied` Opportunities after 7 days """
- auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15
+ auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index 23ccb7e..ae1f36e 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -20,7 +20,6 @@
"configuration_cb",
"shipping_account_head",
"section_break_12",
- "nexus_address",
"nexus"
],
"fields": [
@@ -87,16 +86,12 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "nexus",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Nexus List"
},
{
- "fieldname": "nexus_address",
- "fieldtype": "HTML",
- "label": "Nexus Address"
- },
- {
"fieldname": "nexus",
"fieldtype": "Table",
"label": "Nexus",
@@ -108,19 +103,20 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
}
],
"issingle": 1,
"links": [],
- "modified": "2021-11-08 18:02:29.232090",
+ "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
+ "modified": "2021-11-30 11:17:24.647979",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index b9f24b6..d4bbe88 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -16,9 +16,9 @@
class TaxJarSettings(Document):
def on_update(self):
- TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
- TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
- TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+ TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
+ TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
+ TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index e8aac1d..05c46c5 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -248,20 +248,18 @@
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
- "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
+ "erpnext.regional.saudi_arabia.utils.create_qr_code",
"erpnext.erpnext_integrations.taxjar_integration.create_transaction"
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
- "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
- ],
- "on_trash": [
- "erpnext.regional.check_deletion_permission",
+ "erpnext.erpnext_integrations.taxjar_integration.delete_transaction",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
+ "on_trash": "erpnext.regional.check_deletion_permission",
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 79e8f61..88e5ca9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -96,15 +96,8 @@
'user': self.user_id
})
- if employee_user_permission_exists: return
-
- employee_user_permission_exists = frappe.db.exists('User Permission', {
- 'allow': 'Employee',
- 'for_value': self.name,
- 'user': self.user_id
- })
-
- if employee_user_permission_exists: return
+ if employee_user_permission_exists:
+ return
add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id)
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index 8a8e8db..7aac2b6 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.functions import Sum
from frappe.utils import flt, nowdate
import erpnext
@@ -41,24 +42,34 @@
self.status = "Cancelled"
def set_total_advance_paid(self):
- paid_amount = frappe.db.sql("""
- select ifnull(sum(debit), 0) as paid_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].paid_amount
+ gle = frappe.qb.DocType("GL Entry")
- return_amount = frappe.db.sql("""
- select ifnull(sum(credit), 0) as return_amount
- from `tabGL Entry`
- where against_voucher_type = 'Employee Advance'
- and voucher_type != 'Expense Claim'
- and against_voucher = %s
- and party_type = 'Employee'
- and party = %s
- """, (self.name, self.employee), as_dict=1)[0].return_amount
+ paid_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.debit).as_("paid_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].paid_amount or 0
+
+ return_amount = (
+ frappe.qb.from_(gle)
+ .select(Sum(gle.credit).as_("return_amount"))
+ .where(
+ (gle.against_voucher_type == 'Employee Advance')
+ & (gle.voucher_type != 'Expense Claim')
+ & (gle.against_voucher == self.name)
+ & (gle.party_type == 'Employee')
+ & (gle.party == self.employee)
+ & (gle.docstatus == 1)
+ & (gle.is_cancelled == 0)
+ )
+ ).run(as_dict=True)[0].return_amount or 0
if paid_amount != 0:
paid_amount = flt(paid_amount) / flt(self.exchange_rate)
diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
index 4ecfa60..5f2e720 100644
--- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py
+++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py
@@ -34,6 +34,24 @@
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
+ def test_paid_amount_on_pe_cancellation(self):
+ employee_name = make_employee("_T@employe.advance")
+ advance = make_employee_advance(employee_name)
+
+ pe = make_payment_entry(advance)
+ pe.submit()
+
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 1000)
+ self.assertEqual(advance.status, "Paid")
+
+ pe.cancel()
+ advance.reload()
+
+ self.assertEqual(advance.paid_amount, 0)
+ self.assertEqual(advance.status, "Unpaid")
+
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index 287dfba..64eee40 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -2,7 +2,6 @@
# See license.txt
import unittest
-from datetime import date
import frappe
from frappe.utils import add_days, getdate
@@ -12,16 +11,14 @@
class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
- make_employee("employee2@transfers.com")
- make_employee("employee3@transfers.com")
create_company()
- create_employee()
- create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self):
+ make_employee("employee2@transfers.com")
+
transfer_obj = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"),
@@ -43,6 +40,8 @@
self.assertEqual(transfer.docstatus, 1)
def test_new_employee_creation(self):
+ make_employee("employee3@transfers.com")
+
transfer = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"),
@@ -63,60 +62,51 @@
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
- name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
- doc = frappe.get_doc("Employee",name)
+ employee = make_employee("employee4@transfers.com",
+ company="Test Company",
+ date_of_birth=getdate("30-09-1980"),
+ date_of_joining=getdate("01-10-2021"),
+ department="Accounts - TC",
+ designation="Accountant"
+ )
+ transfer = create_employee_transfer(employee)
+
count = 0
department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"]
- dt = [getdate("01-10-2021"), date.today()]
+ dt = [getdate("01-10-2021"), getdate()]
- for data in doc.internal_work_history:
+ employee = frappe.get_doc("Employee", employee)
+ for data in employee.internal_work_history:
self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count])
count = count + 1
- data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
- doc = frappe.get_doc("Employee Transfer", data[0]["name"])
- doc.cancel()
- employee_doc = frappe.get_doc("Employee",name)
+ transfer.cancel()
+ employee.reload()
- for data in employee_doc.internal_work_history:
+ for data in employee.internal_work_history:
self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0])
-def create_employee():
- doc = frappe.get_doc({
- "doctype": "Employee",
- "first_name": "John",
- "company": "Test Company",
- "gender": "Male",
- "date_of_birth": getdate("30-09-1980"),
- "date_of_joining": getdate("01-10-2021"),
- "department": "Accounts - TC",
- "designation": "Accountant"
- })
-
- doc.save()
def create_company():
- exists = frappe.db.exists("Company", "Test Company")
- if not exists:
- doc = frappe.get_doc({
- "doctype": "Company",
- "company_name": "Test Company",
- "default_currency": "INR",
- "country": "India"
- })
+ if not frappe.db.exists("Company", "Test Company"):
+ frappe.get_doc({
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India"
+ }).insert()
- doc.save()
-def create_employee_transfer():
+def create_employee_transfer(employee):
doc = frappe.get_doc({
"doctype": "Employee Transfer",
- "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
- "transfer_date": date.today(),
+ "employee": employee,
+ "transfer_date": getdate(),
"transfer_details": [
{
"property": "Designation",
@@ -134,4 +124,6 @@
})
doc.save()
- doc.submit()
\ No newline at end of file
+ doc.submit()
+
+ return doc
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
index 70a48f9..6edbcb5c 100644
--- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
+++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json
@@ -94,7 +94,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Sanctioned Amount",
- "no_copy": 1,
"oldfieldname": "sanctioned_amount",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -120,7 +119,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-11-26 14:23:45.539922",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Claim Detail",
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 4e829a3..5177302 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -19,8 +19,8 @@
validate_active_employee(self.employee)
self.validate_overlapping_dates()
- if self.end_date and self.end_date <= self.start_date:
- frappe.throw(_("End Date must not be lesser than Start Date"))
+ if self.end_date:
+ self.validate_from_to_dates('start_date', 'end_date')
def validate_overlapping_dates(self):
if not self.name:
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f4a88dc..f590d68 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -21,9 +21,10 @@
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'
@@ -376,6 +377,7 @@
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 +771,7 @@
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/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index 647c14b..4e1a464 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -178,8 +178,9 @@
},
{
"fieldname": "batch_size",
- "fieldtype": "Int",
- "label": "Batch Size"
+ "fieldtype": "Float",
+ "label": "Batch Size",
+ "read_only": 1
},
{
"fieldname": "sequence_id",
@@ -200,7 +201,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-24 04:52:54.295168",
+ "modified": "2021-11-29 16:37:18.824489",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Operation",
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index cf19cbf..090a3e7 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -89,7 +89,7 @@
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
+ details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/__init__.py
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
new file mode 100644
index 0000000..b2428e8
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js
@@ -0,0 +1,70 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Work Order Consumed Materials"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Work Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Work Order",
+ get_query: function() {
+ return {
+ filters: {
+ status: ["in", ["In Process", "Completed", "Stopped"]]
+ }
+ }
+ }
+ },
+ {
+ label: __("Production Item"),
+ fieldname: "production_item",
+ fieldtype: "Link",
+ depends_on: "eval: !doc.name",
+ options: "Item"
+ },
+ {
+ label: __("Status"),
+ fieldname: "status",
+ fieldtype: "Select",
+ options: ["In Process", "Completed", "Stopped"]
+ },
+ {
+ label: __("Excess Materials Consumed"),
+ fieldname: "show_extra_consumed_materials",
+ fieldtype: "Check"
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "raw_material_name" && data && data.extra_consumed_qty > 0 ) {
+ value = `<div style="color:red">${value}</div>`;
+ }
+
+ return value;
+ },
+};
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
new file mode 100644
index 0000000..2fc986a
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.json
@@ -0,0 +1,30 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-11-22 17:36:11.886939",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": "Gadgets International",
+ "modified": "2021-11-22 17:36:14.999091",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Work Order Consumed Materials",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Work Order",
+ "report_name": "Work Order Consumed Materials",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Manufacturing User"
+ },
+ {
+ "role": "Stock User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
new file mode 100644
index 0000000..0528348
--- /dev/null
+++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ fields = get_fields()
+ filters = get_filter_condition(report_filters)
+
+ wo_items = {}
+ for d in frappe.get_all("Work Order", filters = filters, fields=fields):
+ d.extra_consumed_qty = 0.0
+ if d.consumed_qty and d.consumed_qty > d.required_qty:
+ d.extra_consumed_qty = d.consumed_qty - d.required_qty
+
+ if d.extra_consumed_qty or not report_filters.show_extra_consumed_materials:
+ wo_items.setdefault((d.name, d.production_item), []).append(d)
+
+ data = []
+ for key, wo_data in wo_items.items():
+ for index, row in enumerate(wo_data):
+ if index != 0:
+ #If one work order has multiple raw materials then show parent data in the first row only
+ for field in ["name", "status", "production_item", "qty", "produced_qty"]:
+ row[field] = ""
+
+ data.append(row)
+
+ return data
+
+def get_fields():
+ return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code",
+ "`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`",
+ "`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`",
+ "`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`",
+ "`tabWork Order`.`produced_qty`"]
+
+def get_filter_condition(report_filters):
+ filters = {
+ "docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]),
+ "creation": ("between", [report_filters.from_date, report_filters.to_date])
+ }
+
+ for field in ["name", "production_item", "company", "status"]:
+ value = report_filters.get(field)
+ if value:
+ key = f"`{field}`"
+ filters.update({key: value})
+
+ return filters
+
+def get_columns():
+ return [
+ {
+ "label": _("Id"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Work Order",
+ "width": 80
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Production Item"),
+ "fieldname": "production_item",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 130
+ },
+ {
+ "label": _("Qty to Produce"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Produced Qty"),
+ "fieldname": "produced_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Raw Material Item"),
+ "fieldname": "raw_material_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 150
+ },
+ {
+ "label": _("Item Name"),
+ "fieldname": "raw_material_name",
+ "width": 130
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Transferred Qty"),
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ },
+ {
+ "label": _("Extra Consumed Qty"),
+ "fieldname": "extra_consumed_qty",
+ "fieldtype": "Float",
+ "width": 100
+ }
+ ]
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index cfa80f8..65b4d02 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,10 +1,6 @@
{
- "charts": [
- {
- "chart_name": "Produced Quantity"
- }
- ],
- "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
+ "charts": [],
+ "content": "[{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"docstatus": 0,
"doctype": "Workspace",
@@ -141,14 +137,6 @@
"type": "Link"
},
{
- "hidden": 0,
- "is_query_report": 0,
- "label": "Reports",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
"dependencies": "Work Order",
"hidden": 0,
"is_query_report": 1,
@@ -295,9 +283,126 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "link_count": 10,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Planning Report",
+ "link_count": 0,
+ "link_to": "Production Planning Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Work Order Summary",
+ "link_count": 0,
+ "link_to": "Work Order Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Quality Inspection",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Quality Inspection Summary",
+ "link_count": 0,
+ "link_to": "Quality Inspection Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Downtime Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Downtime Analysis",
+ "link_count": 0,
+ "link_to": "Downtime Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Job Card",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Job Card Summary",
+ "link_count": 0,
+ "link_to": "Job Card Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Search",
+ "link_count": 0,
+ "link_to": "BOM Search",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Stock Report",
+ "link_count": 0,
+ "link_to": "BOM Stock Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Analytics",
+ "link_count": 0,
+ "link_to": "Production Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Operations Time",
+ "link_count": 0,
+ "link_to": "BOM Operations Time",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Work Order Consumed Materials",
+ "link_count": 0,
+ "link_to": "Work Order Consumed Materials",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2021-08-05 12:16:00.825742",
+ "modified": "2021-11-22 17:55:03.524496",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 897e70c..ee9060b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -278,6 +278,7 @@
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
+erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
@@ -312,3 +313,4 @@
erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
+erpnext.patches.v14_0.migrate_crm_settings
diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py
index b3ef5af..5e1df14 100644
--- a/erpnext/patches/v13_0/check_is_income_tax_component.py
+++ b/erpnext/patches/v13_0/check_is_income_tax_component.py
@@ -3,9 +3,9 @@
import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
import erpnext
-from erpnext.regional.india.setup import setup
def execute():
@@ -30,7 +30,14 @@
frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India":
- setup(patch=True)
+ create_custom_field('Salary Component',
+ dict(fieldname='component_type',
+ label='Component Type',
+ fieldtype='Select',
+ insert_after='description',
+ options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
+ depends_on='eval:doc.type == "Deduction"')
+ )
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
new file mode 100644
index 0000000..5487a6c
--- /dev/null
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -0,0 +1,15 @@
+import frappe
+from frappe import _
+
+
+def execute():
+ selling_settings = frappe.get_single("Selling Settings")
+
+ if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
+ selling_settings.customer_group = None
+
+ if selling_settings.territory in (_("All Territories"), "All Territories"):
+ selling_settings.territory = None
+
+ selling_settings.flags.ignore_mandatory=True
+ selling_settings.save(ignore_permissions=True)
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
new file mode 100644
index 0000000..30d3ea0
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+ settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
+ 'campaign_naming_by',
+ 'close_opportunity_after_days',
+ 'default_valid_till'
+ ], as_dict=True)
+
+ frappe.reload_doc('crm', 'doctype', 'crm_settings')
+ frappe.db.set_value('CRM Settings', 'CRM Settings', {
+ 'campaign_naming_by': settings.campaign_naming_by,
+ 'close_opportunity_after_days': settings.close_opportunity_after_days,
+ 'default_valid_till': settings.default_valid_till
+ })
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index 2cf9d38..0415690 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -1,7 +1,7 @@
import unittest
import frappe
-from frappe.utils import add_days, getdate, nowdate
+from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import (
@@ -13,21 +13,26 @@
class TestProjectProfitability(unittest.TestCase):
-
def setUp(self):
+ frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
+
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
+
make_salary_structure_for_timesheet(emp, company='_Test Company')
- self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
+ date = getdate()
+
+ self.timesheet = make_timesheet(emp, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
- holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
+
+ holidays = self.salary_slip.get_holidays_for_employee(date, date)
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
- self.sales_invoice.due_date = nowdate()
+ self.sales_invoice.due_date = date
self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
@@ -63,6 +68,4 @@
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
- frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
- frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
- frappe.get_doc("Timesheet", self.timesheet.name).cancel()
+ frappe.db.rollback()
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 0cfc008..773d53c 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1106,7 +1106,7 @@
$.each(this.frm.doc.taxes || [], function(i, d) {
if(d.charge_type == "Actual") {
frappe.model.set_value(d.doctype, d.name, "tax_amount",
- flt(d.tax_amount) / flt(exchange_rate));
+ flt(d.base_tax_amount) / flt(exchange_rate));
}
});
}
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
index 1677e9b..7a3854c 100644
--- a/erpnext/public/scss/point-of-sale.scss
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -495,6 +495,11 @@
font-size: var(--text-md);
}
+ > .item-qty-total-container {
+ @extend .net-total-container;
+ padding: 5px 0px 0px 0px;
+ }
+
> .taxes-container {
display: none;
flex-direction: column;
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
index 9a72410..0f08978 100644
--- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -82,7 +82,6 @@
memberships = frappe.db.get_all('Membership', {
'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
- 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 4bd9195..bbca772 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -206,28 +206,18 @@
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
-
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.company_gstin:
- return party_details
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
- get_tax_template_based_on_category(master_doctype, company, party_details)
+ tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
- if party_details.get('taxes_and_charges'):
- return party_details
-
- if not party_details.supplier_gstin:
- return party_details
+ if tax_template_by_category:
+ party_details.get['taxes_and_charges'] = tax_template_by_category
+ return
if not party_details.place_of_supply: return party_details
- if not party_details.company_gstin: return party_details
-
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
"Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])):
@@ -237,6 +227,7 @@
if not default_tax:
return party_details
+
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
@@ -268,9 +259,7 @@
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
'name')
- if default_tax:
- party_details["taxes_and_charges"] = default_tax
- party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
+ return default_tax
def get_tax_template(master_doctype, company, is_inter_state, state_code):
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
@@ -569,17 +558,17 @@
}
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
- for hsn_code, taxable_amount in hsn_taxable_amount.items():
+ for item_or_hsn, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict()
- if not hsn_code:
+ if not item_or_hsn:
frappe.throw(_('GST HSN Code does not exist for one or more items'))
- item_data.hsnCode = int(hsn_code)
+ item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn
item_data.taxableAmount = taxable_amount
item_data.qtyUnit = ""
for attr in item_data_attrs:
item_data[attr] = 0
- for account, tax_detail in hsn_wise_charges.get(hsn_code, {}).items():
+ for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items():
account_type = gst_accounts.get(account, '')
for tax_acc, attrs in tax_map.items():
if account_type == tax_acc:
@@ -847,13 +836,11 @@
doc.get('items')[item_count - 1].taxable_value += diff
def get_depreciation_amount(asset, depreciable_value, row):
- depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
-
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
- depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
- flt(row.expected_value_after_useful_life)) / depreciation_left
+ depreciation_amount = (flt(asset.gross_purchase_amount) -
+ flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
diff --git a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
index 681f72f..8e9a728 100644
--- a/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
+++ b/erpnext/regional/print_format/ksa_vat_invoice/ksa_vat_invoice.json
@@ -10,14 +10,14 @@
"docstatus": 0,
"doctype": "Print Format",
"font_size": 14,
- "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set tax_amount = frappe.utils.flt(data_object[item.item_code][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[item.item_code][0])-%}\n <span>{{ frappe.format(data_object[item.item_code][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[item.item_code][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
+ "html": "<div class=\"ksa-vat-format\">\n <div class=\"qr-flex\">\n <div style=\"qr-flex: 1\">\n <h2 class=\"invoice-heading\">TAX INVOICE</h2>\n <h2 class=\"invoice-heading\">\u0641\u0627\u062a\u0648\u0631\u0629 \u0636\u0631\u064a\u0628\u064a\u0629</h2>\n </div>\n \n <img class=\"qr-code\" src={{doc.qr_code}}>\n </div>\n {% set company = frappe.get_doc(\"Company\", doc.company)%}\n {% if (doc.company_address) %}\n {% set supplier_address_doc = frappe.get_doc('Address', doc.company_address) %}\n {% endif %}\n \n {% if(doc.customer_address) %}\n {% set customer_address = frappe.get_doc('Address', doc.customer_address ) %}\n {% endif %}\n \n {% if(doc.shipping_address_name) %}\n {% set customer_shipping_address = frappe.get_doc('Address', doc.shipping_address_name ) %}\n {% endif %} \n \n <table class=\"ksa-invoice-table two-columns\">\n <thead>\n <tr>\n <th>{{ company.name }}</th>\n <th style=\"text-align: right;\">{{ company.company_name_in_arabic }}</th>\n </tr>\n </thead>\n\n <tbody>\n <!-- Invoice Info -->\n <tr>\n <td>Invoice#: {{doc.name}}</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.name}}</td>\n </tr>\n <tr>\n <td>Invoice Date: {{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u0641\u0627\u062a\u0648\u0631\u0629: {{doc.posting_date}}</td>\n </tr>\n <tr>\n <td>Date of Supply:{{doc.posting_date}}</td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0644\u062a\u0648\u0631\u064a\u062f: {{doc.posting_date}}</td>\n </tr>\n \n <!--Supplier Info -->\n <tr>\n <td>Supplier:</td>\n <td>\u0627\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n\t\t{% if (company.tax_id) %}\n <tr>\n <td>Supplier Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0645\u0648\u0631\u062f:</td>\n </tr>\n <tr>\n <td>{{ company.tax_id }}</td>\n <td>{{ company.tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td>{{ company.name }}</td>\n <td>{{ company.company_name_in_arabic }} </td>\n </tr>\n \n \n {% if(supplier_address_doc) %}\n <tr>\n <td>{{ supplier_address_doc.address_line1}} </td>\n <td>{{ supplier_address_doc.address_in_arabic}} </td>\n </tr>\n <tr>\n <td>Phone: {{ supplier_address_doc.phone }}</td>\n <td>\u0647\u0627\u062a\u0641: {{ supplier_address_doc.phone }}</td>\n </tr>\n <tr>\n <td>Email: {{ supplier_address_doc.email_id }}</td>\n <td>\u0628\u0631\u064a\u062f \u0627\u0644\u0643\u062a\u0631\u0648\u0646\u064a: {{ supplier_address_doc.email_id }}</td>\n </tr>\n {% endif %}\n \n <!-- Customer Info -->\n <tr>\n <td>CUSTOMER:</td>\n <td>\u0639\u0645\u064a\u0644:</td>\n </tr>\n\t\t{% set customer_tax_id = frappe.db.get_value('Customer', doc.customer, 'tax_id') %}\n\t\t{% if customer_tax_id %}\n <tr>\n <td>Customer Tax Identification Number:</td>\n <td>\u0631\u0642\u0645 \u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0636\u0631\u064a\u0628\u064a \u0644\u0644\u0639\u0645\u064a\u0644:</td>\n </tr>\n <tr>\n <td>{{ customer_tax_id }}</td>\n <td>{{ customer_tax_id }}</td>\n </tr>\n {% endif %}\n <tr>\n <td> {{ doc.customer }}</td>\n <td> {{ doc.customer_name_in_arabic }} </td>\n </tr>\n \n {% if(customer_address) %}\n <tr>\n <td>{{ customer_address.address_line1}} </td>\n <td>{{ customer_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n {% if(customer_shipping_address) %}\n <tr>\n <td>SHIPPING ADDRESS:</td>\n <td>\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0634\u062d\u0646:</td>\n </tr>\n \n <tr>\n <td>{{ customer_shipping_address.address_line1}} </td>\n <td>{{ customer_shipping_address.address_in_arabic}} </td>\n </tr>\n {% endif %}\n \n\t\t{% if(doc.po_no) %}\n <tr>\n <td>OTHER INFORMATION</td>\n <td>\u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u062e\u0631\u0649</td>\n </tr>\n \n <tr>\n <td>Purchase Order Number: {{ doc.po_no }}</td>\n <td>\u0631\u0642\u0645 \u0623\u0645\u0631 \u0627\u0644\u0634\u0631\u0627\u0621: {{ doc.po_no }}</td>\n </tr>\n {% endif %}\n \n <tr>\n <td>Payment Due Date: {{ doc.due_date}} </td>\n <td>\u062a\u0627\u0631\u064a\u062e \u0627\u0633\u062a\u062d\u0642\u0627\u0642 \u0627\u0644\u062f\u0641\u0639: {{ doc.due_date}}</td>\n </tr>\n </tbody>\n </table>\n\n <!--Dynamic Colspan for total row columns-->\n {% set col = namespace(one = 2, two = 1) %}\n {% set length = doc.taxes | length %}\n {% set length = length / 2 | round %}\n {% set col.one = col.one + length %}\n {% set col.two = col.two + length %}\n \n {%- if(doc.taxes | length % 2 > 0 ) -%}\n {% set col.two = col.two + 1 %}\n {% endif %}\n \n <!-- Items -->\n {% set total = namespace(amount = 0) %}\n <table class=\"ksa-invoice-table\">\n <thead>\n <tr>\n <th>Nature of goods or services <br />\u0637\u0628\u064a\u0639\u0629 \u0627\u0644\u0633\u0644\u0639 \u0623\u0648 \u0627\u0644\u062e\u062f\u0645\u0627\u062a</th>\n <th>\n Unit price <br />\n \u0633\u0639\u0631 \u0627\u0644\u0648\u062d\u062f\u0629\n </th>\n <th>\n Quantity <br />\n \u0627\u0644\u0643\u0645\u064a\u0629\n </th>\n <th>\n Taxable Amount <br />\n \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u062e\u0627\u0636\u0639 \u0644\u0644\u0636\u0631\u064a\u0628\u0629\n </th>\n \n {% for row in doc.taxes %}\n <th style=\"min-width: 130px\">{{row.description}}</th>\n {% endfor %}\n \n <th>\n Total <br />\n \u0627\u0644\u0645\u062c\u0645\u0648\u0639\n </th>\n </tr>\n </thead>\n <tbody>\n {%- for item in doc.items -%}\n {% set total.amount = item.amount %}\n <tr>\n <td>{{ item.item_code or item.item_name }}</td>\n <td>{{ item.get_formatted(\"rate\") }}</td>\n <td>{{ item.qty }}</td>\n <td>{{ item.get_formatted(\"amount\") }}</td>\n {% for row in doc.taxes %}\n {% set data_object = json.loads(row.item_wise_tax_detail) %}\n {% set key = item.item_code or item.item_name %}\n {% set tax_amount = frappe.utils.flt(data_object[key][1]/doc.conversion_rate, row.precision('tax_amount')) %}\n <td>\n <div class=\"qr-flex\">\n {%- if(data_object[key][0])-%}\n <span>{{ frappe.format(data_object[key][0], {'fieldtype': 'Percent'}) }}</span>\n {%- endif -%}\n <span>\n {%- if(data_object[key][1])-%}\n {{ frappe.format_value(tax_amount, currency=doc.currency) }}</span>\n {% set total.amount = total.amount + tax_amount %}\n {%- endif -%}\n </div>\n </td>\n {% endfor %}\n <td>{{ frappe.format_value(frappe.utils.flt(total.amount, doc.precision('total_taxes_and_charges')), currency=doc.currency) }}</td>\n </tr>\n {%- endfor -%}\n </tbody>\n <tfoot>\n <tr>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n \n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0627\u0644\u0625\u062c\u0645\u0627\u0644\u064a \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n <br />\n \u0625\u062c\u0645\u0627\u0644\u064a \u0636\u0631\u064a\u0628\u0629 \u0627\u0644\u0642\u064a\u0645\u0629 \u0627\u0644\u0645\u0636\u0627\u0641\u0629\n </td>\n <td colspan={{ col.two }}>\n Total (Excluding VAT)\n <br />\n Total VAT\n </td>\n <td>\n {{ doc.get_formatted(\"total\") }} <br />\n {{ doc.get_formatted(\"total_taxes_and_charges\") }}\n </td>\n </tr>\n <tr>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n <td colspan={{ col.one }} class=\"qr-rtl\">\n \u0625\u062c\u0645\u0627\u0644\u064a \u0627\u0644\u0645\u0628\u0644\u063a \u0627\u0644\u0645\u0633\u062a\u062d\u0642</td>\n <td colspan={{ col.two }}>Total Amount Due</td>\n <td>{{ doc.get_formatted(\"grand_total\") }}</td>\n </tr>\n </tfoot>\n </table>\n\n\t{%- if doc.terms -%}\n <p>\n {{doc.terms}}\n </p>\n\t{%- endif -%}\n</div>\n",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2021-11-22 10:40:24.716932",
+ "modified": "2021-11-29 13:47:37.870818",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Invoice",
diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py
index 91a4767..f3fe5e8 100644
--- a/erpnext/regional/report/eway_bill/eway_bill.py
+++ b/erpnext/regional/report/eway_bill/eway_bill.py
@@ -106,14 +106,14 @@
row.update({'ship_to_state': row.to_state})
def set_taxes(row, filters):
- taxes = frappe.get_list("Sales Taxes and Charges",
+ taxes = frappe.get_all("Sales Taxes and Charges",
filters={
'parent': row.dn_id
},
fields=('item_wise_tax_detail', 'account_head'))
account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"]
- taxes_list = frappe.get_list("GST Account",
+ taxes_list = frappe.get_all("GST Account",
filters={
"parent": "GST Settings",
"company": filters.company
diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
index 5a281a4..17e5064 100644
--- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py
+++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py
@@ -41,7 +41,7 @@
return self.columns, self.data
def get_sa_vat_accounts(self):
- self.sa_vat_accounts = frappe.get_list("South Africa VAT Account",
+ self.sa_vat_accounts = frappe.get_all("South Africa VAT Account",
filters = {"parent": self.filters.company}, pluck="account")
if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings")
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
index a2f634e..7d00d8b 100644
--- a/erpnext/regional/saudi_arabia/utils.py
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -1,7 +1,10 @@
import io
import os
+from base64 import b64encode
import frappe
+from frappe import _
+from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
@@ -28,28 +31,80 @@
for field in meta.get_image_fields():
if field.fieldname == 'qr_code':
- from urllib.parse import urlencode
+ ''' TLV conversion for
+ 1. Seller's Name
+ 2. VAT Number
+ 3. Time Stamp
+ 4. Invoice Amount
+ 5. VAT Amount
+ '''
+ tlv_array = []
+ # Sellers Name
- # Creating public url to print format
- default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
+ seller_name = frappe.db.get_value(
+ 'Company',
+ doc.company,
+ 'company_name_in_arabic')
- # System Language
- language = frappe.get_system_settings('language')
+ if not seller_name:
+ frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
- params = urlencode({
- 'format': default_print_format or 'Standard',
- '_lang': language,
- 'key': doc.get_signature()
- })
+ tag = bytes([1]).hex()
+ length = bytes([len(seller_name.encode('utf-8'))]).hex()
+ value = seller_name.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
- # creating qr code for the url
- url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?{ params }"
+ # VAT Number
+ tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
+ if not tax_id:
+ frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
+
+ tag = bytes([2]).hex()
+ length = bytes([len(tax_id)]).hex()
+ value = tax_id.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Time Stamp
+ posting_date = getdate(doc.posting_date)
+ time = get_time(doc.posting_time)
+ seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
+ time_stamp = add_to_date(posting_date, seconds=seconds)
+ time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ tag = bytes([3]).hex()
+ length = bytes([len(time_stamp)]).hex()
+ value = time_stamp.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Invoice Amount
+ invoice_amount = str(doc.grand_total)
+ tag = bytes([4]).hex()
+ length = bytes([len(invoice_amount)]).hex()
+ value = invoice_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # VAT Amount
+ vat_amount = str(doc.total_taxes_and_charges)
+
+ tag = bytes([5]).hex()
+ length = bytes([len(vat_amount)]).hex()
+ value = vat_amount.encode('utf-8').hex()
+ tlv_array.append(''.join([tag, length, value]))
+
+ # Joining bytes into one
+ tlv_buff = ''.join(tlv_array)
+
+ # base64 conversion for QR Code
+ base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
+
qr_image = io.BytesIO()
- url = qr_create(url, error='L')
+ url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1)
+ name = frappe.generate_hash(doc.name, 5)
+
# making file
- filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
+ filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({
"doctype": "File",
"file_name": filename,
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index ad788e5..ee5b0ea 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -961,9 +961,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
- "max_attachments": 1,
- "migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e",
- "modified": "2021-10-21 12:58:55.514512",
+ "modified": "2021-11-30 01:33:21.106073",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 769e066..aa83726 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -302,6 +302,109 @@
enable_calculate_bundle_price(enable=0)
+ def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle 1",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+ make_product_bundle("_Test Product Bundle 2",
+ ["_Test Bundle Item 2", "_Test Bundle Item 3"])
+
+ enable_calculate_bundle_price()
+
+ item_list = [
+ {
+ "item_code": "_Test Product Bundle 1",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 2",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ quotation.packed_items[0].rate = 100
+ quotation.packed_items[1].rate = 200
+ quotation.packed_items[2].rate = 200
+ quotation.packed_items[3].rate = 300
+ quotation.save()
+
+ expected_values = [300, 500]
+
+ for item in quotation.items:
+ self.assertEqual(item.amount, expected_values[item.idx-1])
+
+ enable_calculate_bundle_price(enable=0)
+
+ def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ make_item("_Test Product Bundle 1", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 2", {"is_stock_item": 0})
+ make_item("_Test Product Bundle 3", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 3", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle 1",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+ make_product_bundle("_Test Product Bundle 2",
+ ["_Test Bundle Item 2", "_Test Bundle Item 3"])
+ make_product_bundle("_Test Product Bundle 3",
+ ["_Test Bundle Item 3", "_Test Bundle Item 1"])
+
+ item_list = [
+ {
+ "item_code": "_Test Product Bundle 1",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 2",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Product Bundle 3",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ quotation = make_quotation(item_list=item_list, do_not_submit=1)
+ del quotation.items[1]
+ quotation.save()
+
+ for id, item in enumerate(quotation.packed_items):
+ expected_index = id + 1
+ self.assertEqual(item.idx, expected_index)
+
test_records = frappe.get_test_records('Quotation')
def enable_calculate_bundle_price(enable=1):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 7c7ed9a..7e99a06 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -134,6 +134,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1507,16 +1508,23 @@
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:09:51.515542",
+ "modified": "2021-10-05 12:16:40.775704",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 47b8ebd..1c24825 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -925,6 +925,7 @@
"supplier",
"pricing_rules"
],
+ "condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 1e5590e..95f6c4e 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -48,6 +48,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_24",
"net_rate",
"net_amount",
@@ -789,15 +790,23 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:15:05.803091",
+ "modified": "2021-10-05 12:27:25.014789",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index c27f1ea..27bc541 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -11,11 +11,6 @@
"customer_group",
"column_break_4",
"territory",
- "crm_settings_section",
- "campaign_naming_by",
- "default_valid_till",
- "column_break_9",
- "close_opportunity_after_days",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
@@ -44,13 +39,6 @@
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
- "fieldname": "campaign_naming_by",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Campaign Naming By",
- "options": "Campaign Name\nNaming Series\nAuto Name"
- },
- {
"fieldname": "customer_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -72,18 +60,6 @@
"options": "Price List"
},
{
- "default": "15",
- "description": "Auto close Opportunity after the no. of days mentioned above",
- "fieldname": "close_opportunity_after_days",
- "fieldtype": "Int",
- "label": "Close Opportunity After Days"
- },
- {
- "fieldname": "default_valid_till",
- "fieldtype": "Data",
- "label": "Default Quotation Validity Days"
- },
- {
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
@@ -170,15 +146,6 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "crm_settings_section",
- "fieldtype": "Section Break",
- "label": "CRM Settings"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
@@ -204,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-08 19:38:10.175989",
+ "modified": "2021-09-13 12:32:17.004404",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index e7c5e76..fb86e61 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -8,7 +8,6 @@
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
from frappe.utils import cint
-from frappe.utils.nestedset import get_root_of
class SellingSettings(Document):
@@ -37,9 +36,3 @@
editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
-
- def set_default_customer_group_and_territory(self):
- if not self.customer_group:
- self.customer_group = get_root_of('Customer Group')
- if not self.territory:
- self.territory = get_root_of('Territory')
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index a5b2d50..4920584 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -100,6 +100,10 @@
`<div class="add-discount-wrapper">
${this.get_discount_icon()} ${__('Add Discount')}
</div>
+ <div class="item-qty-total-container">
+ <div class="item-qty-total-label">${__('Total Items')}</div>
+ <div class="item-qty-total-value">0.00</div>
+ </div>
<div class="net-total-container">
<div class="net-total-label">${__("Net Total")}</div>
<div class="net-total-value">0.00</div>
@@ -142,6 +146,7 @@
this.$numpad_section.prepend(
`<div class="numpad-totals">
+ <span class="numpad-item-qty-total"></span>
<span class="numpad-net-total"></span>
<span class="numpad-grand-total"></span>
</div>`
@@ -470,6 +475,7 @@
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
+ this.render_total_item_qty(frm.doc.items);
const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
this.render_grand_total(grand_total);
@@ -487,6 +493,21 @@
);
}
+ render_total_item_qty(items) {
+ var total_item_qty = 0;
+ items.map((item) => {
+ total_item_qty = total_item_qty + item.qty;
+ });
+
+ this.$totals_section.find('.item-qty-total-container').html(
+ `<div>${__('Total Quantity')}</div><div>${total_item_qty}</div>`
+ );
+
+ this.$numpad_section.find('.numpad-item-qty-total').html(
+ `<div>${__('Total Quantity')}: <span>${total_item_qty}</span></div>`
+ );
+ }
+
render_grand_total(value) {
const currency = this.events.get_frm().doc.currency;
this.$totals_section.find('.grand-total-container').html(
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 2050478..e2e0db4 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -157,25 +157,19 @@
commission_rate() {
this.calculate_commission();
- refresh_field("total_commission");
}
total_commission() {
- if(this.frm.doc.base_net_total) {
- frappe.model.round_floats_in(this.frm.doc, ["base_net_total", "total_commission"]);
+ frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
- if(this.frm.doc.base_net_total < this.frm.doc.total_commission) {
- var msg = (__("[Error]") + " " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "total_commission",
- this.frm.doc.name)) + " > " +
- __(frappe.meta.get_label(this.frm.doc.doctype, "base_net_total", this.frm.doc.name)));
- frappe.msgprint(msg);
- throw msg;
- }
+ const { amount_eligible_for_commission } = this.frm.doc;
+ if(!amount_eligible_for_commission) return;
- this.frm.set_value("commission_rate",
- flt(this.frm.doc.total_commission * 100.0 / this.frm.doc.base_net_total));
- }
+ this.frm.set_value(
+ "commission_rate", flt(
+ this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
+ )
+ );
}
allocated_percentage(doc, cdt, cdn) {
@@ -185,7 +179,7 @@
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person));
- sales_person.allocated_amount = flt(this.frm.doc.base_net_total *
+ sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person);
@@ -259,28 +253,39 @@
}
calculate_commission() {
- if(this.frm.fields_dict.commission_rate) {
- if(this.frm.doc.commission_rate > 100) {
- var msg = __(frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)) +
- " " + __("cannot be greater than 100");
- frappe.msgprint(msg);
- throw msg;
- }
+ if(!this.frm.fields_dict.commission_rate) return;
- this.frm.doc.total_commission = flt(this.frm.doc.base_net_total * this.frm.doc.commission_rate / 100.0,
- precision("total_commission"));
+ if(this.frm.doc.commission_rate > 100) {
+ this.frm.set_value("commission_rate", 100);
+ frappe.throw(`${__(frappe.meta.get_label(
+ this.frm.doc.doctype, "commission_rate", this.frm.doc.name
+ ))} ${__("cannot be greater than 100")}`);
}
+
+ this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
+ (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
+ )
+
+ this.frm.doc.total_commission = flt(
+ this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
+ precision("total_commission")
+ );
+
+ refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_contribution() {
var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person);
- if(sales_person.allocated_percentage) {
- sales_person.allocated_amount = flt(
- me.frm.doc.base_net_total * sales_person.allocated_percentage / 100.0,
- precision("allocated_amount", sales_person));
- }
+ if (!sales_person.allocated_percentage) return;
+
+ sales_person.allocated_amount = flt(
+ me.frm.doc.amount_eligible_for_commission
+ * sales_person.allocated_percentage
+ / 100.0,
+ precision("allocated_amount", sales_person)
+ );
});
}
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 95ca386..91f60fb 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -12,6 +12,10 @@
}
});
}
+
+ frm.call('check_if_transactions_exist').then(r => {
+ frm.toggle_enable("default_currency", (!r.message));
+ });
},
setup: function(frm) {
erpnext.company.setup_queries(frm);
@@ -87,9 +91,6 @@
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
- frm.toggle_enable("default_currency", (frm.doc.__onload &&
- !frm.doc.__onload.transactions_exist));
-
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
frm.add_custom_button(__('Cost Centers'), function() {
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index dedd2d3..e739739 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -22,8 +22,8 @@
def onload(self):
load_address_and_contact(self, "company")
- self.get("__onload")["transactions_exist"] = self.check_if_transactions_exist()
+ @frappe.whitelist()
def check_if_transactions_exist(self):
exists = False
for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation",
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 503aeac..98f9119 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -303,7 +303,6 @@
def update_selling_defaults():
selling_settings = frappe.get_doc("Selling Settings")
- selling_settings.set_default_customer_group_and_territory()
selling_settings.cust_master_name = "Customer Name"
selling_settings.so_required = "No"
selling_settings.dn_required = "No"
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 1478007..cad4c54 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -53,6 +53,7 @@
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains()
+ set_defaults_for_tests()
frappe.db.commit()
@@ -127,6 +128,14 @@
[d.name for d in domains])
add_all_roles_to('Administrator')
+def set_defaults_for_tests():
+ from frappe.utils.nestedset import get_root_of
+
+ selling_settings = frappe.get_single("Selling Settings")
+ selling_settings.customer_group = get_root_of("Customer Group")
+ selling_settings.territory = get_root_of("Territory")
+ selling_settings.save()
+
def insert_record(records):
for r in records:
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 1412acf..e47837f 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -10,7 +10,7 @@
"idx": 0,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-10-26 21:32:55.323591",
+ "modified": "2021-11-05 21:32:55.323591",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -123,6 +123,13 @@
"label": "Products Settings",
"link_to": "Products Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "crm",
+ "label": "CRM Settings",
+ "link_to": "CRM Settings",
+ "type": "DocType"
}
],
"title": "ERPNext Settings"
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index ed8c878..0da45a5 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -22,7 +22,7 @@
'customer_group')
bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings',
'allow_stale'))
- bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings',
+ bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings',
'default_valid_till'))
# if no company, show a dialog box to create a new company
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index da9c66d..a33134b 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -6,7 +6,7 @@
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import flt, nowdate
+from frappe.utils import flt
class Bin(Document):
@@ -127,33 +127,11 @@
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
- '''Called from erpnext.stock.utils.update_bin'''
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
+ from erpnext.stock.stock_ledger import repost_current_voucher
+
update_qty(bin_name, args)
-
- if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
- if not args.get("posting_date"):
- args["posting_date"] = nowdate()
-
- if args.get("is_cancelled") and via_landed_cost_voucher:
- return
-
- # Reposts only current voucher SL Entries
- # Updates valuation rate, stock value, stock queue for current transaction
- update_entries_after({
- "item_code": args.get('item_code'),
- "warehouse": args.get('warehouse'),
- "posting_date": args.get("posting_date"),
- "posting_time": args.get("posting_time"),
- "voucher_type": args.get("voucher_type"),
- "voucher_no": args.get("voucher_no"),
- "sle_id": args.get('name'),
- "creation": args.get('creation')
- }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
- # update qty in future sle and Validate negative qty
- update_qty_in_future_sle(args, allow_negative_stock)
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index ad1b3b4..55a4c95 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -145,6 +145,7 @@
"sales_team_section_break",
"sales_partner",
"column_break7",
+ "amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break1",
@@ -1302,16 +1303,23 @@
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "amount_eligible_for_commission",
+ "fieldtype": "Currency",
+ "label": "Amount Eligible for Commission",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-08 14:29:13.428984",
+ "modified": "2021-10-09 14:29:13.428984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index a96c299..51c88be 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -49,6 +49,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
+ "grant_commission",
"section_break_25",
"net_rate",
"net_amount",
@@ -753,13 +754,20 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-10-05 12:12:44.018872",
+ "modified": "2021-10-06 12:12:44.018872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 4b314a0..4f4e691 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -88,6 +88,7 @@
"sales_details",
"sales_uom",
"is_sales_item",
+ "grant_commission",
"column_break3",
"max_discount",
"deferred_revenue",
@@ -1020,6 +1021,12 @@
"fieldname": "website_image_alt",
"fieldtype": "Data",
"label": "Image Description"
+ },
+ {
+ "default": "1",
+ "fieldname": "grant_commission",
+ "fieldtype": "Check",
+ "label": "Grant Commission"
}
],
"has_web_view": 1,
@@ -1028,8 +1035,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "max_attachments": 1,
- "modified": "2021-10-27 21:04:00.324786",
+ "modified": "2021-12-03 08:32:03.869294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1097,7 +1103,7 @@
"search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
- "sort_field": "idx desc,modified desc",
+ "sort_field": "modified",
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 5daabe8..decf522 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -222,10 +222,11 @@
'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
def validate_website_image(self):
+ """Validate if the website image is a public file"""
+
if frappe.flags.in_import:
return
- """Validate if the website image is a public file"""
auto_set_website_image = False
if not self.website_image and self.image:
auto_set_website_image = True
@@ -255,10 +256,11 @@
self.website_image = None
def make_thumbnail(self):
+ """Make a thumbnail of `website_image`"""
+
if frappe.flags.in_import:
return
- """Make a thumbnail of `website_image`"""
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
@@ -722,7 +724,6 @@
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -736,7 +737,6 @@
repost_stock(new_name, warehouse)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
@frappe.whitelist()
def copy_specification_from_item_group(self):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 7237178..8b1224b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -488,7 +488,7 @@
item_doc.save()
# Check values saved correctly
- barcodes = frappe.get_list(
+ barcodes = frappe.get_all(
'Item Barcode',
fields=['barcode', 'barcode_type'],
filters={'parent': item_code})
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 3f73093..e4091c4 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -108,9 +108,32 @@
packed_items = doc.get("packed_items")
doc.set("packed_items", [])
+
for d in packed_items:
if d not in delete_list:
- doc.append("packed_items", d)
+ add_item_to_packing_list(doc, d)
+
+def add_item_to_packing_list(doc, packed_item):
+ doc.append("packed_items", {
+ 'parent_item': packed_item.parent_item,
+ 'item_code': packed_item.item_code,
+ 'item_name': packed_item.item_name,
+ 'uom': packed_item.uom,
+ 'qty': packed_item.qty,
+ 'rate': packed_item.rate,
+ 'conversion_factor': packed_item.conversion_factor,
+ 'description': packed_item.description,
+ 'warehouse': packed_item.warehouse,
+ 'batch_no': packed_item.batch_no,
+ 'actual_batch_qty': packed_item.actual_batch_qty,
+ 'serial_no': packed_item.serial_no,
+ 'target_warehouse': packed_item.target_warehouse,
+ 'actual_qty': packed_item.actual_qty,
+ 'projected_qty': packed_item.projected_qty,
+ 'incoming_rate': packed_item.incoming_rate,
+ 'prevdoc_doctype': packed_item.prevdoc_doctype,
+ 'parent_detail_docname': packed_item.parent_detail_docname
+ })
def update_product_bundle_price(doc, parent_items):
"""Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
@@ -128,7 +151,8 @@
else:
update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
- bundle_price = 0
+ bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
+ bundle_price = bundle_item.qty * bundle_item_rate
parent_items_index += 1
# for the last product bundle
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 965a32d..01cceb1 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -54,9 +54,11 @@
@frappe.whitelist()
def restart_reposting(self):
- self.set_status('Queued')
- frappe.enqueue(repost, timeout=1800, queue='long',
- job_name='repost_sle', now=True, doc=self)
+ self.set_status('Queued', write=False)
+ self.current_index = 0
+ self.distinct_item_and_warehouse = None
+ self.items_to_be_repost = None
+ self.db_update()
def deduplicate_similar_repost(self):
""" Deduplicate similar reposts based on item-warehouse-posting combination."""
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d31e65a..a38dfa5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -545,7 +545,7 @@
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries
- if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
+ if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 0679467..5ef0770 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -41,6 +41,7 @@
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -582,6 +583,65 @@
self.assertEqual(fg_cost,
flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
+ def test_work_order_manufacture_with_material_consumption(self):
+ from erpnext.manufacturing.doctype.work_order.work_order import (
+ make_stock_entry as _make_stock_entry,
+ )
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
+
+ bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
+ "is_default": 1, "docstatus": 1})
+
+ work_order = frappe.new_doc("Work Order")
+ work_order.update({
+ "company": "_Test Company",
+ "fg_warehouse": "_Test Warehouse 1 - _TC",
+ "production_item": "_Test FG Item",
+ "bom_no": bom_no,
+ "qty": 1.0,
+ "stock_uom": "_Test UOM",
+ "wip_warehouse": "_Test Warehouse - _TC"
+ })
+ work_order.insert()
+ work_order.submit()
+
+ make_stock_entry(item_code="_Test Item",
+ target="Stores - _TC", qty=10, basic_rate=5000.0)
+ make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="Stores - _TC", qty=10, basic_rate=1000.0)
+
+
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+ for d in s.get("items"):
+ d.s_warehouse = "Stores - _TC"
+ s.insert()
+ s.submit()
+
+ # When Stock Entry has RM and FG
+ s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
+ s.save()
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
+ self.assertEqual(fg_cost,
+ flt(rm_cost - scrap_cost, 2))
+
+ # When Stock Entry has only FG + Scrap
+ s.items.pop(0)
+ s.items.pop(0)
+ s.submit()
+
+ rm_cost = 0
+ for d in s.get('items'):
+ if d.s_warehouse:
+ rm_cost += d.amount
+ self.assertEqual(rm_cost, 0)
+ expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
+ fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
+ self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
def test_variant_work_order(self):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
index b7d1497..3402972 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
@@ -153,11 +154,12 @@
"icon": "fa fa-upload-alt",
"idx": 1,
"is_submittable": 1,
- "max_attachments": 1,
- "modified": "2020-04-08 17:02:47.196206",
+ "links": [],
+ "modified": "2021-11-30 01:33:51.437194",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index ca92936..26db264 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -33,65 +33,6 @@
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0)
- def test_warehouse_renaming(self):
- create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
- account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
- self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
-
- # Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
-
- # Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
-
- def test_warehouse_merging(self):
- company = "_Test Company with perpetual inventory"
- create_warehouse("Test Warehouse for Merging 1", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 2", company=company,
- properties={"parent_warehouse": "All Warehouses - TCP1"})
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
- qty=1, rate=100, company=company)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
- qty=1, rate=100, company=company)
-
- existing_bin_qty = (
- cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
- + cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
- )
-
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
- "Test Warehouse for Merging 2 - TCP1", merge=True)
-
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
-
- bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
-
- self.assertEqual(bin_qty, existing_bin_qty)
-
- self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
-
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 9b90932..05076b5 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -1,7 +1,6 @@
{
"actions": [],
"allow_import": 1,
- "allow_rename": 1,
"creation": "2013-03-07 18:50:32",
"description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType",
@@ -245,7 +244,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-04-09 19:54:56.263965",
+ "modified": "2021-12-03 04:40:06.414630",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index b9dbc38..9cfad86 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -10,7 +10,6 @@
from frappe.utils import cint, flt
from frappe.utils.nestedset import NestedSet
-import erpnext
from erpnext.stock import get_warehouse_account
@@ -68,57 +67,6 @@
return frappe.db.sql("""select name from `tabWarehouse`
where parent_warehouse = %s limit 1""", self.name)
- def before_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).before_rename(old_name, new_name, merge)
-
- # Add company abbr if not provided
- new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
-
- if merge:
- if not frappe.db.exists("Warehouse", new_warehouse):
- frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
-
- if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
- frappe.throw(_("Both Warehouse must belong to same Company"))
-
- return new_warehouse
-
- def after_rename(self, old_name, new_name, merge=False):
- super(Warehouse, self).after_rename(old_name, new_name, merge)
-
- new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
- self.db_set("warehouse_name", new_warehouse_name)
-
- if merge:
- self.recalculate_bin_qty(new_name)
-
- def get_new_warehouse_name_without_abbr(self, name):
- company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
- parts = name.rsplit(" - ", 1)
-
- if parts[-1].lower() == company_abbr.lower():
- name = parts[0]
-
- return name
-
- def recalculate_bin_qty(self, new_name):
- from erpnext.stock.stock_balance import repost_stock
- frappe.db.auto_commit_on_many_writes = 1
- existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
-
- repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
- from tabBin where warehouse=%s""", new_name)
-
- # Delete all existing bins to avoid duplicate bins for the same item and warehouse
- frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
-
- for item_code in repost_stock_for_items:
- repost_stock(item_code, new_name)
-
- frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
- frappe.db.auto_commit_on_many_writes = 0
-
def convert_to_group_or_ledger(self):
if self.is_group:
self.convert_to_ledger()
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index e00382b..9889a22 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -299,7 +299,7 @@
"warehouse": warehouse,
"income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults),
"expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) ,
- "discount_account": None or get_default_discount_account(args, item_defaults),
+ "discount_account": get_default_discount_account(args, item_defaults),
"cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults),
'has_serial_no': item.has_serial_no,
'has_batch_no': item.has_batch_no,
@@ -317,6 +317,7 @@
"net_rate": 0.0,
"net_amount": 0.0,
"discount_percentage": 0.0,
+ "discount_amount": 0.0,
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
"update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0,
"delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0,
@@ -326,7 +327,8 @@
"against_blanket_order": args.get("against_blanket_order"),
"bom_no": item.get("default_bom"),
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
- "weight_uom": args.get("weight_uom") or item.get("weight_uom")
+ "weight_uom": args.get("weight_uom") or item.get("weight_uom"),
+ "grant_commission": item.get("grant_commission")
})
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9d40982..d78632a 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -7,9 +7,10 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
import erpnext
+from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
@@ -17,19 +18,15 @@
)
-# future reposting
class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
_exceptions = frappe.local('stockledger_exceptions')
-# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
- from erpnext.stock.utils import update_bin
-
cancel = sl_entries[0].get("is_cancelled")
if cancel:
validate_cancellation(sl_entries)
@@ -64,7 +61,38 @@
# preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
- update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+ is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
+ if is_stock_item:
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_bin_qty(bin_name, args)
+ repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
+ else:
+ frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
+
+def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+ if not args.get("posting_date"):
+ args["posting_date"] = nowdate()
+
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
+ update_entries_after({
+ "item_code": args.get('item_code'),
+ "warehouse": args.get('warehouse'),
+ "posting_date": args.get("posting_date"),
+ "posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
+ "voucher_no": args.get("voucher_no"),
+ "sle_id": args.get('name'),
+ "creation": args.get('creation')
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ # update qty in future sle and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def get_args_for_future_sle(row):
return frappe._dict({
@@ -803,9 +831,9 @@
def update_bin(self):
# update bin for each warehouse
for warehouse, data in self.data.items():
- bin_record = get_or_make_bin(self.item_code, warehouse)
+ bin_name = get_or_make_bin(self.item_code, warehouse)
- frappe.db.set_value('Bin', bin_record, {
+ frappe.db.set_value('Bin', bin_name, {
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 8031c58..72d8098 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -187,7 +187,7 @@
bin_obj.flags.ignore_permissions = True
return bin_obj
-def get_or_make_bin(item_code, warehouse) -> str:
+def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
@@ -203,11 +203,12 @@
return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
- bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
- update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
+ bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index 91df548..fbf2594 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 @@
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