Merge pull request #29115 from deepeshgarg007/deferred_revenue_multi_currency_jv
fix: Deferred revenue booking for multi currency invoices via JV
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 8f93811..4d61f1f 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -40,6 +40,7 @@
- HR
- projects
- support
+ - CRM
- assets
- integrations
- quality
@@ -48,6 +49,7 @@
- agriculture
- education
- non-profit
+ - other
validations:
required: true
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000..3aaba71
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,55 @@
+accounts:
+- erpnext/accounts/*
+- erpnext/controllers/accounts_controller.py
+- erpnext/controllers/taxes_and_totals.py
+
+stock:
+- erpnext/stock/*
+- erpnext/controllers/stock_controller.py
+- erpnext/controllers/item_variant.py
+
+assets:
+- erpnext/assets/*
+
+regional:
+- erpnext/regional/*
+
+selling:
+- erpnext/selling/*
+- erpnext/controllers/selling_controller.py
+
+buying:
+- erpnext/buying/*
+- erpnext/controllers/buying_controller.py
+
+support:
+- erpnext/support/*
+
+POS:
+- pos*
+
+ecommerce:
+- erpnext/e_commerce/*
+
+maintenance:
+- erpnext/maintenance/*
+
+manufacturing:
+- erpnext/manufacturing/*
+
+crm:
+- erpnext/crm/*
+
+HR:
+- erpnext/hr/*
+
+payroll:
+- erpnext/payroll*
+
+projects:
+- erpnext/projects/*
+
+# Any python files modifed but no test files modified
+needs-tests:
+- any: ['erpnext/**/*.py']
+ all: ['!erpnext/**/test*.py']
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index db46c56..b644568 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -12,7 +12,7 @@
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
- python-version: 3.6
+ python-version: 3.8
- name: 'Clone repo'
uses: actions/checkout@v2
diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml
new file mode 100644
index 0000000..a774400
--- /dev/null
+++ b/.github/workflows/labeller.yml
@@ -0,0 +1,12 @@
+name: "Pull Request Labeler"
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ triage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v3
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 33a28ac..d05bbbe 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -34,7 +34,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 186f95e..7347a58 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -46,7 +46,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml
index 3bbf6a9..77d3c1a 100644
--- a/.github/workflows/server-tests-postgres.yml
+++ b/.github/workflows/server-tests-postgres.yml
@@ -46,7 +46,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index d765f04..ab6a53b 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -36,7 +36,7 @@
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.7
+ python-version: 3.8
- uses: actions/setup-node@v2
with:
diff --git a/CODEOWNERS b/CODEOWNERS
index a4a14de..bfc2601 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -23,13 +23,13 @@
erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal @pateljannat
-erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal @pateljannat
-erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
-erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
+erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
+erpnext/public/ @nextchamp-saqib @marination
-.github/ @surajshetty3416 @ankush
+.github/ @ankush
requirements.txt @gavindsouza
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
new file mode 100644
index 0000000..6c40f2b
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Currency Exchange Settings', {
+ service_provider: function(frm) {
+ if (frm.doc.service_provider == "exchangerate.host") {
+ let result = ['result'];
+ let params = {
+ date: '{transaction_date}',
+ from: '{from_currency}',
+ to: '{to_currency}'
+ };
+ add_param(frm, "https://api.exchangerate.host/convert", params, result);
+ } else if (frm.doc.service_provider == "frankfurter.app") {
+ let result = ['rates', '{to_currency}'];
+ let params = {
+ base: '{from_currency}',
+ symbols: '{to_currency}'
+ };
+ add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
+ }
+ }
+});
+
+
+function add_param(frm, api, params, result) {
+ var row;
+ frm.clear_table("req_params");
+ frm.clear_table("result_key");
+
+ frm.doc.api_endpoint = api;
+
+ $.each(params, function(key, value) {
+ row = frm.add_child("req_params");
+ row.key = key;
+ row.value = value;
+ });
+
+ $.each(result, function(key, value) {
+ row = frm.add_child("result_key");
+ row.key = value;
+ });
+
+ frm.refresh_fields();
+}
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
new file mode 100644
index 0000000..7921fcc
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -0,0 +1,126 @@
+{
+ "actions": [],
+ "creation": "2022-01-10 13:03:26.237081",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "api_details_section",
+ "service_provider",
+ "api_endpoint",
+ "url",
+ "column_break_3",
+ "help",
+ "section_break_2",
+ "req_params",
+ "column_break_4",
+ "result_key"
+ ],
+ "fields": [
+ {
+ "fieldname": "api_details_section",
+ "fieldtype": "Section Break",
+ "label": "API Details"
+ },
+ {
+ "fieldname": "api_endpoint",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "API Endpoint",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "Example URL",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "help",
+ "fieldtype": "HTML",
+ "label": "Help",
+ "options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "label": "Request Parameters"
+ },
+ {
+ "fieldname": "req_params",
+ "fieldtype": "Table",
+ "label": "Parameters",
+ "options": "Currency Exchange Settings Details",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "result_key",
+ "fieldtype": "Table",
+ "label": "Result Key",
+ "options": "Currency Exchange Settings Result",
+ "read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
+ "reqd": 1
+ },
+ {
+ "fieldname": "service_provider",
+ "fieldtype": "Select",
+ "label": "Service Provider",
+ "options": "frankfurter.app\nexchangerate.host\nCustom",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2022-01-10 15:51:14.521174",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
new file mode 100644
index 0000000..e16ff3a
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import requests
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import nowdate
+
+
+class CurrencyExchangeSettings(Document):
+ def validate(self):
+ self.set_parameters_and_result()
+ response, value = self.validate_parameters()
+ self.validate_result(response, value)
+
+ def set_parameters_and_result(self):
+ if self.service_provider == 'exchangerate.host':
+ self.set('result_key', [])
+ self.set('req_params', [])
+
+ self.api_endpoint = "https://api.exchangerate.host/convert"
+ self.append('result_key', {'key': 'result'})
+ self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
+ self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
+ self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
+ elif self.service_provider == 'frankfurter.app':
+ self.set('result_key', [])
+ self.set('req_params', [])
+
+ self.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ self.append('result_key', {'key': 'rates'})
+ self.append('result_key', {'key': '{to_currency}'})
+ self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+ self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+
+ def validate_parameters(self):
+ if frappe.flags.in_test:
+ return None, None
+
+ params = {}
+ for row in self.req_params:
+ params[row.key] = row.value.format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )
+
+ api_url = self.api_endpoint.format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )
+
+ try:
+ response = requests.get(api_url, params=params)
+ except requests.exceptions.RequestException as e:
+ frappe.throw("Error: " + str(e))
+
+ response.raise_for_status()
+ value = response.json()
+
+ return response, value
+
+ def validate_result(self, response, value):
+ if frappe.flags.in_test:
+ return
+
+ try:
+ for key in self.result_key:
+ value = value[str(key.key).format(
+ transaction_date=nowdate(),
+ to_currency='INR',
+ from_currency='USD'
+ )]
+ except Exception:
+ frappe.throw("Invalid result key. Response: " + response.text)
+ if not isinstance(value, (int, float)):
+ frappe.throw(_("Returned exchange rate is neither integer not float."))
+
+ self.url = response.url
+ frappe.msgprint("Exchange rate of USD to INR is " + str(value))
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
new file mode 100644
index 0000000..2778729
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings/test_currency_exchange_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+import unittest
+
+
+class TestCurrencyExchangeSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
new file mode 100644
index 0000000..3093587
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
@@ -0,0 +1,39 @@
+{
+ "actions": [],
+ "creation": "2021-09-02 14:54:49.033512",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key",
+ "value"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Value",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:55.889037",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
new file mode 100644
index 0000000..a6ad763
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsDetails(Document):
+ pass
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py b/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/__init__.py
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
new file mode 100644
index 0000000..fff5337
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-09-03 13:17:22.088259",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "key"
+ ],
+ "fields": [
+ {
+ "fieldname": "key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Key",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-11-03 19:14:40.054245",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Currency Exchange Settings Result",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
new file mode 100644
index 0000000..1774128
--- /dev/null
+++ b/erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CurrencyExchangeSettingsResult(Document):
+ pass
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c1b056b..0e07abd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1708,7 +1708,10 @@
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
- if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
+ eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
+ has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
+
+ if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 11d59bc..134bccf 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -353,7 +353,6 @@
if not for_validate and not self.customer:
self.customer = profile.customer
- self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 7d31e0a..56479a0 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -556,6 +556,37 @@
batch.cancel()
batch.delete()
+ def test_ignore_pricing_rule(self):
+ from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
+
+ item_price = frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'item_code': '_Test Item',
+ 'price_list': '_Test Price List',
+ 'price_list_rate': '450',
+ })
+ item_price.insert()
+ pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
+ pr.save()
+ pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
+ pos_inv.items[0].rate = 300
+ pos_inv.save()
+ self.assertEquals(pos_inv.items[0].discount_percentage, 10)
+ # rate shouldn't change
+ self.assertEquals(pos_inv.items[0].rate, 405)
+
+ pos_inv.ignore_pricing_rule = 1
+ pos_inv.items[0].rate = 300
+ pos_inv.save()
+ self.assertEquals(pos_inv.ignore_pricing_rule, 1)
+ # rate should change since pricing rules are ignored
+ self.assertEquals(pos_inv.items[0].rate, 300)
+
+ item_price.delete()
+ pos_inv.delete()
+ pr.delete()
+
+
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index 314c894..5746a84 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -650,7 +650,7 @@
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
- "priority": 1,
+ "priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@@ -676,6 +676,8 @@
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
+ return doc
+
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 545abf7..5062c1c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -651,7 +651,7 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
- "permlevel": 1,
+ "permlevel": 0,
"print_hide": 1
},
{
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-10-21 20:19:38.667508",
+ "modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json
index d2c505c..e032bb3 100644
--- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json
+++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json
@@ -28,14 +28,14 @@
{
"columns": 2,
"fieldname": "single_threshold",
- "fieldtype": "Currency",
+ "fieldtype": "Float",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
- "fieldtype": "Currency",
+ "fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
@@ -59,7 +59,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-31 11:42:12.213977",
+ "modified": "2022-01-13 12:04:42.904263",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
@@ -68,5 +68,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
index a4842c1..3a51db8 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py
@@ -121,20 +121,21 @@
"""
simulate future posting by creating dummy gl entries. starts from the last posting date.
"""
- if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
- self.estimate_for_period_list = get_period_list(
- self.filters.from_fiscal_year,
- self.filters.to_fiscal_year,
- add_days(self.last_entry_date, 1),
- self.period_list[-1].to_date,
- "Date Range",
- "Monthly",
- company=self.filters.company,
- )
- for period in self.estimate_for_period_list:
- amount = self.calculate_amount(period.from_date, period.to_date)
- gle = self.make_dummy_gle(period.key, period.to_date, amount)
- self.gle_entries.append(gle)
+ if self.service_start_date != self.service_end_date:
+ if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
+ self.estimate_for_period_list = get_period_list(
+ self.filters.from_fiscal_year,
+ self.filters.to_fiscal_year,
+ add_days(self.last_entry_date, 1),
+ self.period_list[-1].to_date,
+ "Date Range",
+ "Monthly",
+ company=self.filters.company,
+ )
+ for period in self.estimate_for_period_list:
+ amount = self.calculate_amount(period.from_date, period.to_date)
+ gle = self.make_dummy_gle(period.key, period.to_date, amount)
+ self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self):
"""
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index b296876..010284c 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -167,7 +167,7 @@
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
- "default": 0
+ "default": 1
},
{
"fieldname": "show_opening_entries",
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 385c8b2..7f27920 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -448,9 +448,11 @@
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
- for dim in accounting_dimensions:
- keylist.append(gle.get(dim))
- keylist.append(gle.get("cost_center"))
+ if filters.get("include_dimensions"):
+ for dim in accounting_dimensions:
+ keylist.append(gle.get(dim))
+ keylist.append(gle.get("cost_center"))
+
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@@ -547,10 +549,7 @@
"fieldname": "balance",
"fieldtype": "Float",
"width": 130
- }
- ]
-
- columns.extend([
+ },
{
"label": _("Voucher Type"),
"fieldname": "voucher_type",
@@ -584,7 +583,7 @@
"fieldname": "project",
"width": 100
}
- ])
+ ]
if filters.get("include_dimensions"):
for dim in get_accounting_dimensions(as_list = False):
@@ -594,14 +593,14 @@
"fieldname": dim.fieldname,
"width": 100
})
-
- columns.extend([
- {
+ columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
- },
+ })
+
+ columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
new file mode 100644
index 0000000..78c109a
--- /dev/null
+++ b/erpnext/accounts/test/test_reports.py
@@ -0,0 +1,48 @@
+import unittest
+from typing import List, Tuple
+
+from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
+
+DEFAULT_FILTERS = {
+ "company": "_Test Company",
+ "from_date": "2010-01-01",
+ "to_date": "2030-01-01",
+ "period_start_date": "2010-01-01",
+ "period_end_date": "2030-01-01"
+}
+
+
+REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
+ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"} ),
+ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1} ),
+ ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+ ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
+ ("Consolidated Financial Statement", {"report": "Balance Sheet"} ),
+ ("Consolidated Financial Statement", {"report": "Profit and Loss Statement"} ),
+ ("Consolidated Financial Statement", {"report": "Cash Flow"} ),
+ ("Gross Profit", {"group_by": "Invoice"}),
+ ("Gross Profit", {"group_by": "Item Code"}),
+ ("Gross Profit", {"group_by": "Item Group"}),
+ ("Gross Profit", {"group_by": "Customer"}),
+ ("Gross Profit", {"group_by": "Customer Group"}),
+ ("Item-wise Sales Register", {}),
+ ("Item-wise Purchase Register", {}),
+ ("Sales Register", {}),
+ ("Purchase Register", {}),
+ ("Tax Detail", {"mode": "run", "report_name": "Tax Detail"},),
+]
+
+OPTIONAL_FILTERS = {}
+
+
+class TestReports(unittest.TestCase):
+ def test_execute_all_accounts_reports(self):
+ """Test that all script report in stock modules are executable with supported filters"""
+ for report, filter in REPORT_FILTER_TEST_CASES:
+ execute_script_report(
+ report_name=report,
+ module="Accounts",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index a18b03a..ee3ec8e 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -608,7 +608,17 @@
return purchase_document
def get_fixed_asset_account(self):
- return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
+ fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
+ if not fixed_asset_account:
+ frappe.throw(
+ _("Set {0} in asset category {1} for company {2}").format(
+ frappe.bold("Fixed Asset Account"),
+ frappe.bold(self.asset_category),
+ frappe.bold(self.company),
+ ),
+ title=_("Account not Found"),
+ )
+ return fixed_asset_account
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 05541d1..908d78c 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -1,6 +1,8 @@
import unittest
from functools import partial
+import frappe
+
from erpnext.controllers import queries
@@ -85,3 +87,6 @@
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)
+
+ def test_default_uoms(self):
+ self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py
index 13aa697..f4d3f97 100644
--- a/erpnext/controllers/tests/test_transaction_base.py
+++ b/erpnext/controllers/tests/test_transaction_base.py
@@ -4,19 +4,72 @@
class TestUtils(unittest.TestCase):
- def test_reset_default_field_value(self):
- doc = frappe.get_doc({
- "doctype": "Purchase Receipt",
- "set_warehouse": "Warehouse 1",
- })
+ def test_reset_default_field_value(self):
+ doc = frappe.get_doc({
+ "doctype": "Purchase Receipt",
+ "set_warehouse": "Warehouse 1",
+ })
- # Same values
- doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
- doc.reset_default_field_value("set_warehouse", "items", "warehouse")
- self.assertEqual(doc.set_warehouse, "Warehouse 1")
+ # Same values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, "Warehouse 1")
- # Mixed values
- doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
- doc.reset_default_field_value("set_warehouse", "items", "warehouse")
- self.assertEqual(doc.set_warehouse, None)
+ # Mixed values
+ doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
+ doc.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.assertEqual(doc.set_warehouse, None)
+ def test_reset_default_field_value_in_mfg_stock_entry(self):
+ # manufacture stock entry with rows having blank source/target wh
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ stock_entry_type="Manufacture",
+ company="_Test Company",
+ from_warehouse="_Test Warehouse - _TC",
+ to_warehouse="_Test Warehouse 1 - _TC",
+ items=[
+ frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
+ frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
+ ]
+ )
+ se.save()
+
+ # default fields must be untouched
+ self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
+ self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
+
+ se.delete()
+
+ def test_reset_default_field_value_in_transfer_stock_entry(self):
+ doc = frappe.get_doc({
+ "doctype": "Stock Entry",
+ "purpose": "Material Receipt",
+ "from_warehouse": "Warehouse 1",
+ "to_warehouse": "Warehouse 2",
+ })
+
+ # Same values
+ doc.items = [
+ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
+ ]
+
+ doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+ doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+ self.assertEqual(doc.from_warehouse, "Warehouse 1")
+ self.assertEqual(doc.to_warehouse, "Warehouse 2")
+
+ # Mixed values in source wh
+ doc.items = [
+ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
+ {"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
+ {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
+ ]
+
+ doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+ doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+ self.assertEqual(doc.from_warehouse, None)
+ self.assertEqual(doc.to_warehouse, "Warehouse 2")
\ No newline at end of file
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index d9013b0..636b948 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -201,8 +201,8 @@
conditions = get_event_conditions("Course Schedule", filters)
data = frappe.db.sql("""select name, course, color,
- timestamp(schedule_date, from_time) as from_datetime,
- timestamp(schedule_date, to_time) as to_datetime,
+ timestamp(schedule_date, from_time) as from_time,
+ timestamp(schedule_date, to_time) as to_time,
room, student_group, 0 as 'allDay'
from `tabCourse Schedule`
where ( schedule_date between %(start)s and %(end)s )
diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py
index ffd323d..615d2c4 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule.py
+++ b/erpnext/education/doctype/course_schedule/course_schedule.py
@@ -3,6 +3,8 @@
# For license information, please see license.txt
+from datetime import datetime
+
import frappe
from frappe import _
from frappe.model.document import Document
@@ -30,6 +32,14 @@
if self.from_time > self.to_time:
frappe.throw(_("From Time cannot be greater than To Time."))
+ """Handles specicfic case to update schedule date in calendar """
+ if isinstance(self.from_time, str):
+ try:
+ datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
+ self.schedule_date = datetime_obj
+ except ValueError:
+ pass
+
def validate_overlap(self):
"""Validates overlap for Student Group, Instructor, Room"""
@@ -47,4 +57,4 @@
validate_overlap_for(self, "Assessment Plan", "student_group")
validate_overlap_for(self, "Assessment Plan", "room")
- validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
+ validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
index 803527e..cacd539 100644
--- a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
+++ b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js
@@ -1,11 +1,10 @@
frappe.views.calendar["Course Schedule"] = {
field_map: {
- // from_datetime and to_datetime don't exist as docfields but are used in onload
- "start": "from_datetime",
- "end": "to_datetime",
+ "start": "from_time",
+ "end": "to_time",
"id": "name",
"title": "course",
- "allDay": "allDay"
+ "allDay": "allDay",
},
gantt: false,
order_by: "schedule_date",
diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py
index a732419..56149af 100644
--- a/erpnext/education/doctype/course_schedule/test_course_schedule.py
+++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py
@@ -6,6 +6,7 @@
import frappe
from frappe.utils import to_timedelta, today
+from frappe.utils.data import add_to_date
from erpnext.education.utils import OverlapError
@@ -39,6 +40,11 @@
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
+ def test_update_schedule_date(self):
+ doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
+ doc.schedule_date = add_to_date(doc.schedule_date, days=1)
+ doc.save()
+
def make_course_schedule_test_record(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index cb1b560..2d129c8 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -19,7 +19,7 @@
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
- project = "Employee Onboarding : Test Researcher - test@researcher.com"
+ project = "Employee Onboarding : test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
@@ -27,7 +27,7 @@
onboarding = create_employee_onboarding()
project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
- self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
+ self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
@@ -64,8 +64,8 @@
def get_job_applicant():
- if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
- return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
+ if frappe.db.exists('Job Applicant', 'test@researcher.com'):
+ return frappe.get_doc('Job Applicant', 'test@researcher.com')
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index 200f675..66b609c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -192,10 +192,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-09-29 23:06:10.904260",
+ "modified": "2022-01-12 16:28:53.196881",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -210,10 +211,11 @@
"write": 1
}
],
- "search_fields": "applicant_name",
+ "search_fields": "applicant_name, email_id, job_title, phone_number",
"sender_field": "email_id",
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"subject_field": "notes",
"title_field": "applicant_name"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index abaa50c..5b3d9bf 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -7,6 +7,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.model.naming import append_number_if_name_exists
from frappe.utils import validate_email_address
from erpnext.hr.doctype.interview.interview import get_interviewers
@@ -21,10 +22,11 @@
self.get("__onload").job_offer = job_offer[0].name
def autoname(self):
- keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
- if not keys:
- frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
- self.name = " - ".join(keys)
+ self.name = self.email_id
+
+ # applicant can apply more than once for a different job title or reapply
+ if frappe.db.exists("Job Applicant", self.name):
+ self.name = append_number_if_name_exists("Job Applicant", self.name)
def validate(self):
if self.email_id:
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index 36dcf6b..bf16220 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -9,7 +9,26 @@
class TestJobApplicant(unittest.TestCase):
- pass
+ def test_job_applicant_naming(self):
+ applicant = frappe.get_doc({
+ "doctype": "Job Applicant",
+ "status": "Open",
+ "applicant_name": "_Test Applicant",
+ "email_id": "job_applicant_naming@example.com"
+ }).insert()
+ self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
+
+ applicant = frappe.get_doc({
+ "doctype": "Job Applicant",
+ "status": "Open",
+ "applicant_name": "_Test Applicant",
+ "email_id": "job_applicant_naming@example.com"
+ }).insert()
+ self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
+
+ def tearDown(self):
+ frappe.db.rollback()
+
def create_job_applicant(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json
index 9e895c3..84ce114 100644
--- a/erpnext/hr/doctype/leave_period/leave_period.json
+++ b/erpnext/hr/doctype/leave_period/leave_period.json
@@ -1,294 +1,108 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####",
- "beta": 0,
"creation": "2018-04-13 15:20:52.864288",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "from_date",
+ "to_date",
+ "is_active",
+ "column_break_3",
+ "company",
+ "optional_holiday_list"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "is_active",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Active",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Is Active"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "company",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Company",
- "length": 0,
- "no_copy": 0,
"options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "optional_holiday_list",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Holiday List for Optional Leave",
- "length": 0,
- "no_copy": 0,
- "options": "Holiday List",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Holiday List"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-30 16:15:43.305502",
+ "links": [],
+ "modified": "2022-01-13 13:28:12.951025",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",
- "name_case": "",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
+ "search_fields": "from_date, to_date, company",
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index 3373350..27f0540 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -113,10 +113,11 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2021-03-01 17:54:01.014509",
+ "modified": "2022-01-13 13:37:11.218882",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -164,5 +165,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "employee_name",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
index 8b954c4..6b75817 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
@@ -48,7 +48,16 @@
if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date();
}
- }
+ },
+ get_query() {
+ let filters = {"is_active": 1};
+ if (cur_dialog.fields_dict.company.value)
+ filters["company"] = cur_dialog.fields_dict.company.value;
+
+ return {
+ filters: filters
+ };
+ },
},
{
fieldtype: "Column Break"
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index f82d9a0..5a60fb7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -530,16 +530,6 @@
row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_rate)
- if self.routing:
- time_in_mins = flt(frappe.db.get_value("BOM Operation", {
- "workstation": row.workstation,
- "operation": row.operation,
- "parent": self.routing
- }, ["time_in_mins"]))
-
- if time_in_mins:
- row.time_in_mins = time_in_mins
-
if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index e90b0a7..8bd60ea 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -46,6 +46,7 @@
wo_doc.delete()
def test_update_bom_operation_time(self):
+ """Update cost shouldn't update routing times."""
operations = [
{
"operation": "Test Operation A",
@@ -85,8 +86,8 @@
routing_doc.save()
bom_doc.update_cost()
bom_doc.reload()
- self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
- self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
+ self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
+ self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
def setup_operations(rows):
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 9926b15..e7eb9c6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
-from frappe.utils import add_months, cint, flt, now, today
+from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
@@ -12,6 +12,7 @@
OverProductionError,
StockOverProductionError,
close_work_order,
+ make_job_card,
make_stock_entry,
stop_unstop,
)
@@ -804,6 +805,34 @@
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
+ # Partial Job Card 1 with qty 10
+ wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1)
+ job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
+ update_job_card(job_card, 10)
+
+ stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ for row in stock_entry.items:
+ if row.is_scrap_item:
+ self.assertEqual(row.qty, 2)
+
+ # Partial Job Card 2 with qty 10
+ operations = []
+ wo_order.load_from_db()
+ for row in wo_order.operations:
+ n_dict = row.as_dict()
+ n_dict['qty'] = 10
+ n_dict['pending_qty'] = 10
+ operations.append(n_dict)
+
+ make_job_card(wo_order.name, operations)
+ job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name')
+ update_job_card(job_card, 10)
+
+ stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ for row in stock_entry.items:
+ if row.is_scrap_item:
+ self.assertEqual(row.qty, 2)
+
def test_close_work_order(self):
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
'Test RM Item 2 for Closed WO']
@@ -883,7 +912,8 @@
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
-def update_job_card(job_card):
+def update_job_card(job_card, jc_qty=None):
+ employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
{
@@ -896,8 +926,12 @@
},
])
+ if jc_qty:
+ job_card_doc.for_quantity = jc_qty
+
job_card_doc.append('time_logs', {
'from_time': now(),
+ 'employee': employee,
'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity
})
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 99741eb..fe62050 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -279,6 +279,7 @@
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.trim_whitespace_from_serial_nos
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
@@ -304,6 +305,7 @@
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.update_job_card_status
+erpnext.patches.v13_0.enable_uoms
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.healthcare_deprecation_warning
erpnext.patches.v13_0.item_naming_series_not_mandatory
@@ -322,3 +324,5 @@
erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v13_0.update_exchange_rate_settings
+erpnext.patches.v14_0.rearrange_company_fields
diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py
new file mode 100644
index 0000000..4d3f637
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_uoms.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('setup', 'doctype', 'uom')
+
+ uom = frappe.qb.DocType("UOM")
+
+ (frappe.qb
+ .update(uom)
+ .set(uom.enabled, 1)
+ .where(uom.creation >= "2021-10-18") # date when this field was released
+ ).run()
diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
new file mode 100644
index 0000000..8a9633d
--- /dev/null
+++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
@@ -0,0 +1,65 @@
+import frappe
+
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+
+def execute():
+ broken_sles = frappe.db.sql("""
+ select name, serial_no
+ from `tabStock Ledger Entry`
+ where
+ is_cancelled = 0
+ and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s)
+ """,
+ (
+ " %", # leading whitespace
+ "% ", # trailing whitespace
+ "%\n %", # leading whitespace on newline
+ "% \n%", # trailing whitespace on newline
+ ),
+ as_dict=True,
+ )
+
+ frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
+
+ if not broken_sles:
+ return
+
+ broken_serial_nos = set()
+
+ # patch SLEs
+ for sle in broken_sles:
+ serial_no_list = get_serial_nos(sle.serial_no)
+ correct_sr_no = "\n".join(serial_no_list)
+
+ if correct_sr_no == sle.serial_no:
+ continue
+
+ frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
+ broken_serial_nos.update(serial_no_list)
+
+ if not broken_serial_nos:
+ return
+
+ # Patch serial No documents if they don't have purchase info
+ # Purchase info is used for fetching incoming rate
+ broken_sr_no_records = frappe.get_list("Serial No",
+ filters={
+ "status":"Active",
+ "name": ("in", broken_serial_nos),
+ "purchase_document_type": ("is", "not set")
+ },
+ pluck="name",
+ )
+
+ frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
+
+ patch_savepoint = "serial_no_patch"
+ for serial_no in broken_sr_no_records:
+ try:
+ frappe.db.savepoint(patch_savepoint)
+ sn = frappe.get_doc("Serial No", serial_no)
+ sn.update_serial_no_reference()
+ sn.db_update()
+ except Exception:
+ frappe.db.rollback(save_point=patch_savepoint)
diff --git a/erpnext/patches/v13_0/update_exchange_rate_settings.py b/erpnext/patches/v13_0/update_exchange_rate_settings.py
new file mode 100644
index 0000000..b7ec232
--- /dev/null
+++ b/erpnext/patches/v13_0/update_exchange_rate_settings.py
@@ -0,0 +1,10 @@
+import frappe
+
+from erpnext.setup.install import setup_currency_exchange
+
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings")
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_result")
+ frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_details")
+ setup_currency_exchange()
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
index 28fc01b..3a4f8f5 100644
--- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py
+++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
@@ -47,3 +47,18 @@
frappe.delete_doc("DocType", doctype, ignore_missing=True)
frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True)
+
+ custom_fields = {
+ 'Sales Invoice': ['patient', 'patient_name', 'ref_practitioner'],
+ 'Sales Invoice Item': ['reference_dt', 'reference_dn'],
+ 'Stock Entry': ['inpatient_medication_entry'],
+ 'Stock Entry Detail': ['patient', 'inpatient_medication_entry_child'],
+ }
+ for doc, fields in custom_fields.items():
+ filters = {
+ 'dt': doc,
+ 'fieldname': ['in', fields]
+ }
+ records = frappe.get_all('Custom Field', filters=filters, pluck='name')
+ for record in records:
+ frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/rearrange_company_fields.py b/erpnext/patches/v14_0/rearrange_company_fields.py
new file mode 100644
index 0000000..dd953ff
--- /dev/null
+++ b/erpnext/patches/v14_0/rearrange_company_fields.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ frappe.reload_doc('setup', 'doctype', 'company')
+
+ custom_fields = {
+ 'Company': [
+ dict(fieldname='hra_section', label='HRA Settings',
+ fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ dict(fieldname='basic_component', label='Basic Component',
+ fieldtype='Link', options='Salary Component', insert_after='hra_section'),
+ dict(fieldname='hra_component', label='HRA Component',
+ fieldtype='Link', options='Salary Component', insert_after='basic_component'),
+ dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
+ dict(fieldname='arrear_component', label='Arrear Component',
+ fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
+ dict(fieldname='non_profit_section', label='Non Profit Settings',
+ fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
+ dict(fieldname='company_80g_number', label='80G Number',
+ fieldtype='Data', insert_after='non_profit_section'),
+ dict(fieldname='with_effect_from', label='80G With Effect From',
+ fieldtype='Date', insert_after='company_80g_number'),
+ dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
+ dict(fieldname='pan_details', label='PAN Number',
+ fieldtype='Data', insert_after='non_profit_column_break')
+ ]
+ }
+
+ create_custom_fields(custom_fields, update=True)
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index ed3fa5b..db88c06 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -61,6 +61,8 @@
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name)))
+ self.db_set("salary_slips_created", 0)
+ self.db_set("salary_slips_submitted", 0)
def get_emp_list(self):
"""
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 9b1ea04..8fa0538 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -102,7 +102,7 @@
frappe.throw(_("Completed On cannot be greater than Today"))
def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
+ depends_on_tasks = ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index c0dcb70..4b99421 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -567,16 +567,16 @@
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component',
- fieldtype='Link', options='Salary Component', insert_after='hra_component'),
+ fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
dict(fieldname='non_profit_section', label='Non Profit Settings',
- fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
dict(fieldname='pan_details', label='PAN Number',
- fieldtype='Data', insert_after='with_effect_from')
+ fieldtype='Data', insert_after='non_profit_column_break')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index ef2bdb6..4b98978 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -53,7 +53,8 @@
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") },
- { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }
+ { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") },
+ { "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") }
],
"default": "B2B"
}
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 11b684d..e50ff18 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -40,7 +40,8 @@
port_code,
shipping_bill_number,
shipping_bill_date,
- reason_for_issuing_document
+ reason_for_issuing_document,
+ company_gstin
"""
def run(self):
@@ -62,6 +63,8 @@
self.get_b2c_data()
elif self.filters.get("type_of_business") == "Advances":
self.get_advance_data()
+ elif self.filters.get("type_of_business") == "NIL Rated":
+ self.get_nil_rated_invoices()
elif self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
@@ -91,6 +94,57 @@
row= [key[0], key[1], value[0], value[1]]
self.data.append(row)
+ def get_nil_rated_invoices(self):
+ nil_exempt_output = [
+ {
+ "description": "Inter-State supplies to registered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Intra-State supplies to registered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Inter-State supplies to unregistered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ },
+ {
+ "description": "Intra-State supplies to unregistered persons",
+ "nil_rated": 0.0,
+ "exempted": 0.0,
+ "non_gst": 0.0
+ }
+ ]
+
+ for invoice, details in self.nil_exempt_non_gst.items():
+ invoice_detail = self.invoices.get(invoice)
+ if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"):
+ if is_inter_state(invoice_detail):
+ nil_exempt_output[0]["nil_rated"] += details[0]
+ nil_exempt_output[0]["exempted"] += details[1]
+ nil_exempt_output[0]["non_gst"] += details[2]
+ else:
+ nil_exempt_output[1]["nil_rated"] += details[0]
+ nil_exempt_output[1]["exempted"] += details[1]
+ nil_exempt_output[1]["non_gst"] += details[2]
+ else:
+ if is_inter_state(invoice_detail):
+ nil_exempt_output[2]["nil_rated"] += details[0]
+ nil_exempt_output[2]["exempted"] += details[1]
+ nil_exempt_output[2]["non_gst"] += details[2]
+ else:
+ nil_exempt_output[3]["nil_rated"] += details[0]
+ nil_exempt_output[3]["exempted"] += details[1]
+ nil_exempt_output[3]["non_gst"] += details[2]
+
+ self.data = nil_exempt_output
+
def get_b2c_data(self):
b2cs_output = {}
@@ -240,10 +294,11 @@
def get_invoice_items(self):
self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict()
+ self.nil_exempt_non_gst = {}
items = frappe.db.sql("""
- select item_code, parent, taxable_value, base_net_amount, item_tax_rate
- from `tab%s Item`
+ select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
+ is_non_gst from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -260,6 +315,16 @@
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
tax_rate_dict.append(rate)
+ if d.is_nil_exempt:
+ self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ if item_tax_rate:
+ self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0)
+ else:
+ self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0)
+ elif d.is_non_gst:
+ self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0)
+
def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql("""
select
@@ -322,21 +387,24 @@
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self):
- self.tax_columns = [
- {
- "fieldname": "rate",
- "label": "Rate",
- "fieldtype": "Int",
- "width": 60
- },
- {
- "fieldname": "taxable_value",
- "label": "Taxable Value",
- "fieldtype": "Currency",
- "width": 100
- }
- ]
self.other_columns = []
+ self.tax_columns = []
+
+ if self.filters.get("type_of_business") != "NIL Rated":
+ self.tax_columns = [
+ {
+ "fieldname": "rate",
+ "label": "Rate",
+ "fieldtype": "Int",
+ "width": 60
+ },
+ {
+ "fieldname": "taxable_value",
+ "label": "Taxable Value",
+ "fieldtype": "Currency",
+ "width": 100
+ }
+ ]
if self.filters.get("type_of_business") == "B2B":
self.invoice_columns = [
@@ -705,6 +773,33 @@
"width": 100
}
]
+ elif self.filters.get("type_of_business") == "NIL Rated":
+ self.invoice_columns = [
+ {
+ "fieldname": "description",
+ "label": "Description",
+ "fieldtype": "Data",
+ "width": 420
+ },
+ {
+ "fieldname": "nil_rated",
+ "label": "Nil Rated",
+ "fieldtype": "Currency",
+ "width": 200
+ },
+ {
+ "fieldname": "exempted",
+ "label": "Exempted",
+ "fieldtype": "Currency",
+ "width": 200
+ },
+ {
+ "fieldname": "non_gst",
+ "label": "Non GST",
+ "fieldtype": "Currency",
+ "width": 200
+ }
+ ]
self.columns = self.invoice_columns + self.tax_columns + self.other_columns
@@ -768,6 +863,11 @@
out = get_advances_json(res, gstin)
gst_json["at"] = out
+ elif filters["type_of_business"] == "NIL Rated":
+ res = report_data[:-1]
+ out = get_exempted_json(res)
+ gst_json["nil"] = out
+
return {
'report_name': report_name,
'report_type': filters['type_of_business'],
@@ -980,6 +1080,36 @@
return out
+def get_exempted_json(data):
+ out = {
+ "inv": [
+ {
+ "sply_ty": "INTRB2B"
+ },
+ {
+ "sply_ty": "INTRAB2B"
+ },
+ {
+ "sply_ty": "INTRB2C"
+ },
+ {
+ "sply_ty": "INTRAB2C"
+ }
+ ]
+ }
+
+ for i, v in enumerate(data):
+ if data[i].get('nil_rated'):
+ out['inv'][i]['nil_amt'] = data[i]['nil_rated']
+
+ if data[i].get('exempted'):
+ out['inv'][i]['expt_amt'] = data[i]['exempted']
+
+ if data[i].get('non_gst'):
+ out['inv'][i]['ngsup_amt'] = data[i]['non_gst']
+
+ return out
+
def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY':
@@ -1064,3 +1194,9 @@
frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
+
+def is_inter_state(invoice_detail):
+ if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
+ return True
+ else:
+ return False
\ No newline at end of file
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 0c0acc7..b2bf546 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -68,7 +68,8 @@
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
soi.warehouse as warehouse,
- so.company, soi.name
+ so.company, soi.name,
+ soi.description as description
FROM
`tabSales Order` so,
(`tabSales Order Item` soi
@@ -184,6 +185,12 @@
"options": "Item",
"width": 100
})
+ columns.append({
+ "label":_("Description"),
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "width": 100
+ })
columns.extend([
{
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 45e8dcc..dd185fc 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -213,6 +213,9 @@
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
+ ["default_deferred_expense_account", {}],
+ ["default_deferred_revenue_account", {}],
+ ["default_expense_claim_payable_account", {}],
["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 2b007e9..06a79b4 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -62,8 +62,13 @@
if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'):
if test_exchange_values.get(kwargs['params']['date']):
return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200)
+ elif args[0].startswith("https://frankfurter.app") and kwargs.get('params'):
+ if kwargs['params'].get('base') and kwargs['params'].get('symbols'):
+ date = args[0].replace("https://frankfurter.app/", "")
+ if test_exchange_values.get(date):
+ return PatchResponse({'rates': {kwargs['params'].get('symbols'): test_exchange_values.get(date)}}, 200)
- return PatchResponse({'result': None}, 404)
+ return PatchResponse({'rates': None}, 404)
@mock.patch('requests.get', side_effect=patched_requests_get)
class TestCurrencyExchange(unittest.TestCase):
@@ -102,6 +107,41 @@
self.assertFalse(exchange_rate == 60)
self.assertEqual(flt(exchange_rate, 3), 65.1)
+ def test_exchange_rate_via_exchangerate_host(self, mock_get):
+ save_new_records(test_records)
+
+ # Update Currency Exchange Rate
+ settings = frappe.get_single("Currency Exchange Settings")
+ settings.service_provider = 'exchangerate.host'
+ settings.save()
+
+ # Update exchange
+ frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
+
+ # Start with allow_stale is True
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
+ self.assertEqual(flt(exchange_rate, 3), 60.0)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
+ self.assertEqual(exchange_rate, 65.1)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
+ self.assertEqual(exchange_rate, 62.9)
+
+ # Exchange rate as on 15th Dec, 2015
+ self.clear_cache()
+ exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
+ self.assertFalse(exchange_rate == 60)
+ self.assertEqual(flt(exchange_rate, 3), 66.999)
+
+ exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
+ self.assertFalse(exchange_rate == 60)
+ self.assertEqual(flt(exchange_rate, 3), 65.1)
+
+ settings = frappe.get_single("Currency Exchange Settings")
+ settings.service_provider = 'frankfurter.app'
+ settings.save()
+
def test_exchange_rate_strict(self, mock_get):
# strict currency settings
frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 86c9b3f..bafaab8 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -60,6 +60,22 @@
frappe.db.set_default("date_format", "dd-mm-yyyy")
+ setup_currency_exchange()
+
+def setup_currency_exchange():
+ ces = frappe.get_single('Currency Exchange Settings')
+ try:
+ ces.set('result_key', [])
+ ces.set('req_params', [])
+
+ ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
+ ces.append('result_key', {'key': 'rates'})
+ ces.append('result_key', {'key': '{to_currency}'})
+ ces.append('req_params', {'key': 'base', 'value': '{from_currency}'})
+ ces.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
+ ces.save()
+ except frappe.ValidationError:
+ pass
def create_compact_item_print_custom_field():
create_custom_field('Print Settings', {
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 336b51c..9dbf49e 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -353,7 +353,8 @@
"doctype": "UOM",
"uom_name": _(d.get("uom_name")),
"name": _(d.get("uom_name")),
- "must_be_whole_number": d.get("must_be_whole_number")
+ "must_be_whole_number": d.get("must_be_whole_number"),
+ "enabled": 1,
}).db_insert()
# bootstrap uom conversion factors
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index cad4c54..4441bb9 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -100,15 +100,21 @@
if not value:
import requests
- api_url = "https://api.exchangerate.host/convert"
- response = requests.get(api_url, params={
- "date": transaction_date,
- "from": from_currency,
- "to": to_currency
- })
+ settings = frappe.get_cached_doc('Currency Exchange Settings')
+ req_params = {
+ "transaction_date": transaction_date,
+ "from_currency": from_currency,
+ "to_currency": to_currency
+ }
+ params = {}
+ for row in settings.req_params:
+ params[row.key] = format_ces_api(row.value, req_params)
+ response = requests.get(format_ces_api(settings.api_endpoint, req_params), params=params)
# expire in 6 hours
response.raise_for_status()
- value = response.json()["result"]
+ value = response.json()
+ for res_key in settings.result_key:
+ value = value[format_ces_api(str(res_key.key), req_params)]
cache.setex(name=key, time=21600, value=flt(value))
return flt(value)
except Exception:
@@ -116,6 +122,13 @@
frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date))
return 0.0
+def format_ces_api(data, param):
+ return data.format(
+ transaction_date=param.get("transaction_date"),
+ to_currency=param.get("to_currency"),
+ from_currency=param.get("from_currency")
+ )
+
def enable_all_roles_and_domains():
""" enable all roles and domain for testing """
# add all roles to users
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index decf522..281e881 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -492,18 +492,20 @@
context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
def add_default_uom_in_conversion_factor_table(self):
- uom_conv_list = [d.uom for d in self.get("uoms")]
- if self.stock_uom not in uom_conv_list:
- ch = self.append('uoms', {})
- ch.uom = self.stock_uom
- ch.conversion_factor = 1
+ if not self.is_new() and self.has_value_changed("stock_uom"):
+ self.uoms = []
+ frappe.msgprint(
+ _("Successfully changed Stock UOM, please redefine conversion factors for new UOM."),
+ alert=True,
+ )
- to_remove = []
- for d in self.get("uoms"):
- if d.conversion_factor == 1 and d.uom != self.stock_uom:
- to_remove.append(d)
+ uoms_list = [d.uom for d in self.get("uoms")]
- [self.remove(d) for d in to_remove]
+ if self.stock_uom not in uoms_list:
+ self.append("uoms", {
+ "uom": self.stock_uom,
+ "conversion_factor": 1
+ })
def update_show_in_website(self):
if self.disabled:
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 4028d93..0957ce0 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -584,6 +584,16 @@
except frappe.ValidationError as e:
self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
+ def test_erasure_of_old_conversions(self):
+ item = create_item("_item change uom")
+ item.stock_uom = "Gram"
+ item.append("uoms", frappe._dict(uom="Box", conversion_factor=2))
+ item.save()
+ item.reload()
+ item.stock_uom = "Nos"
+ item.save()
+ self.assertEqual(len(item.uoms), 1)
+
def test_validate_stock_item(self):
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 93e303c..a61b319 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -8,6 +8,7 @@
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
import erpnext
@@ -85,8 +86,11 @@
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
- self.mark_finished_and_scrap_items()
- self.validate_finished_goods()
+
+ if self.purpose in ("Manufacture", "Repack"):
+ self.mark_finished_and_scrap_items()
+ self.validate_finished_goods()
+
self.validate_with_material_request()
self.validate_batch()
self.validate_inspection()
@@ -109,8 +113,12 @@
self.set_actual_qty()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
- self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
- self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
+
+ if not self.get("purpose") == "Manufacture":
+ # ignore scrap item wh difference and empty source/target wh
+ # in Manufacture Entry
+ self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
+ self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def on_submit(self):
self.update_stock_ledger()
@@ -701,26 +709,25 @@
validate_bom_no(item_code, d.bom_no)
def mark_finished_and_scrap_items(self):
- if self.purpose in ("Repack", "Manufacture"):
- if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
- return
+ if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+ return
- finished_item = self.get_finished_item()
+ finished_item = self.get_finished_item()
- if not finished_item and self.purpose == "Manufacture":
- # In case of independent Manufacture entry, don't auto set
- # user must decide and set
- return
+ if not finished_item and self.purpose == "Manufacture":
+ # In case of independent Manufacture entry, don't auto set
+ # user must decide and set
+ return
- for d in self.items:
- if d.t_warehouse and not d.s_warehouse:
- if self.purpose=="Repack" or d.item_code == finished_item:
- d.is_finished_item = 1
- else:
- d.is_scrap_item = 1
+ for d in self.items:
+ if d.t_warehouse and not d.s_warehouse:
+ if self.purpose=="Repack" or d.item_code == finished_item:
+ d.is_finished_item = 1
else:
- d.is_finished_item = 0
- d.is_scrap_item = 0
+ d.is_scrap_item = 1
+ else:
+ d.is_finished_item = 0
+ d.is_scrap_item = 0
def get_finished_item(self):
finished_item = None
@@ -733,9 +740,9 @@
def validate_finished_goods(self):
"""
- 1. Check if FG exists
- 2. Check if Multiple FG Items are present
- 3. Check FG Item and Qty against WO if present
+ 1. Check if FG exists (mfg, repack)
+ 2. Check if Multiple FG Items are present (mfg)
+ 3. Check FG Item and Qty against WO if present (mfg)
"""
production_item, wo_qty, finished_items = None, 0, []
@@ -748,8 +755,9 @@
for d in self.get('items'):
if d.is_finished_item:
if not self.work_order:
+ # Independent MFG Entry/ Repack Entry, no WO to match against
finished_items.append(d.item_code)
- continue # Independent Manufacture Entry, no WO to match against
+ continue
if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
@@ -762,19 +770,17 @@
finished_items.append(d.item_code)
- if len(set(finished_items)) > 1:
+ if not finished_items:
frappe.throw(
- msg=_("Multiple items cannot be marked as finished item"),
- title=_("Note"),
- exc=FinishedGoodError
+ msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
+ title=_("Missing Finished Good"), exc=FinishedGoodError
)
if self.purpose == "Manufacture":
- if not finished_items:
+ if len(set(finished_items)) > 1:
frappe.throw(
- msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
- title=_("Missing Finished Good"),
- exc=FinishedGoodError
+ msg=_("Multiple items cannot be marked as finished item"),
+ title=_("Note"), exc=FinishedGoodError
)
allowance_percentage = flt(
@@ -1275,22 +1281,29 @@
if not self.pro_doc:
self.set_work_order_details()
- scrap_items = frappe.db.sql('''
- SELECT
- JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
- FROM
- `tabJob Card` JC, `tabJob Card Scrap Item` JCSI
- WHERE
- JCSI.parent = JC.name AND JC.docstatus = 1
- AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
- GROUP BY
- JCSI.item_code
- ''', self.work_order, as_dict=1)
-
- pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
- if pending_qty <=0:
+ if not self.pro_doc.operations:
return []
+ job_card = frappe.qb.DocType('Job Card')
+ job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
+
+ scrap_items = (
+ frappe.qb.from_(job_card)
+ .select(
+ Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
+ job_card_scrap_item.item_code, job_card_scrap_item.item_name,
+ job_card_scrap_item.description, job_card_scrap_item.stock_uom)
+ .join(job_card_scrap_item)
+ .on(job_card_scrap_item.parent == job_card.name)
+ .where(
+ (job_card_scrap_item.item_code.isnotnull())
+ & (job_card.work_order == self.work_order)
+ & (job_card.docstatus == 1))
+ .groupby(job_card_scrap_item.item_code)
+ ).run(as_dict=1)
+
+ pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
+
used_scrap_items = self.get_used_scrap_items()
for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
@@ -1304,6 +1317,9 @@
return scrap_items
+ def get_completed_job_card_qty(self):
+ return flt(min([d.completed_qty for d in self.pro_doc.operations]))
+
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index b874874..8f5d442 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -226,9 +226,47 @@
mtn.cancel()
- def test_repack_no_change_in_valuation(self):
- company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
+ def test_repack_multiple_fg(self):
+ "Test `is_finished_item` for one item repacked into two items."
+ make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
+ repack = frappe.copy_doc(test_records[3])
+ repack.posting_date = nowdate()
+ repack.posting_time = nowtime()
+
+ repack.items[0].qty = 100.0
+ repack.items[0].transfer_qty = 100.0
+ repack.items[1].qty = 50.0
+
+ repack.append("items", {
+ "conversion_factor": 1.0,
+ "cost_center": "_Test Cost Center - _TC",
+ "doctype": "Stock Entry Detail",
+ "expense_account": "Stock Adjustment - _TC",
+ "basic_rate": 150,
+ "item_code": "_Test Item 2",
+ "parentfield": "items",
+ "qty": 50.0,
+ "stock_uom": "_Test UOM",
+ "t_warehouse": "_Test Warehouse - _TC",
+ "transfer_qty": 50.0,
+ "uom": "_Test UOM"
+ })
+ repack.set_stock_entry_type()
+ repack.insert()
+
+ self.assertEqual(repack.items[1].is_finished_item, 1)
+ self.assertEqual(repack.items[2].is_finished_item, 1)
+
+ repack.items[1].is_finished_item = 0
+ repack.items[2].is_finished_item = 0
+
+ # must raise error if 0 fg in repack entry
+ self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
+
+ repack.delete() # teardown
+
+ def test_repack_no_change_in_valuation(self):
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
qty=50, basic_rate=100)
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
index bfbffe2..4dbb0e7 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js
@@ -111,6 +111,7 @@
filters: [
['DocType', 'issingle', '=', 0],
['DocType', 'istable', '=', 0],
+ ['DocType', 'is_submittable', '=', 0],
['DocType', 'name', 'not in', invalid_doctypes],
['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
]
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index b3348f1..de8f506 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -29,6 +29,7 @@
class ServiceLevelAgreement(Document):
def validate(self):
+ self.validate_selected_doctype()
self.validate_doc()
self.validate_status_field()
self.check_priorities()
@@ -106,6 +107,23 @@
frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(
frappe.bold(self.entity_type), frappe.bold(self.entity)))
+ def validate_selected_doctype(self):
+ invalid_doctypes = list(frappe.model.core_doctypes_list)
+ invalid_doctypes.extend(['Cost Center', 'Company'])
+ valid_document_types = frappe.get_all('DocType', {
+ 'issingle': 0,
+ 'istable': 0,
+ 'is_submittable': 0,
+ 'name': ['not in', invalid_doctypes],
+ 'module': ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
+ }, pluck="name")
+
+ if self.document_type not in valid_document_types:
+ frappe.throw(
+ msg=_("Please select valid document type."),
+ title=_("Invalid Document Type")
+ )
+
def validate_status_field(self):
meta = frappe.get_meta(self.document_type)
if not meta.get_field("status"):
@@ -247,9 +265,15 @@
]
customer = doc.get('customer')
- or_filters.append(
- ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)]
- )
+ if customer:
+ or_filters.extend([
+ ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)],
+ ["Service Level Agreement", "entity_type", "is", "not set"]
+ ])
+ else:
+ or_filters.append(
+ ["Service Level Agreement", "entity_type", "is", "not set"]
+ )
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter,
@@ -361,11 +385,18 @@
sla = get_active_service_level_agreement_for(doc)
if not sla:
+ remove_sla_if_applied(doc)
return
process_sla(doc, sla)
+def remove_sla_if_applied(doc):
+ doc.service_level_agreement = None
+ doc.response_by = None
+ doc.resolution_by = None
+
+
def process_sla(doc, sla):
if not doc.creation:
@@ -853,7 +884,7 @@
@frappe.whitelist()
def get_sla_doctypes():
doctypes = []
- data = frappe.get_list('Service Level Agreement',
+ data = frappe.get_all('Service Level Agreement',
{'enabled': 1},
['document_type'],
distinct=1
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
deleted file mode 100644
index 22e2c37..0000000
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from frappe import _
-
-
-def get_data():
- return {
- 'fieldname': 'service_level_agreement',
- 'transactions': [
- {
- 'label': _('Issue'),
- 'items': ['Issue']
- }
- ]
- }
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index b07c862..a34124f 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -244,6 +244,13 @@
applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
self.assertEqual(applied_sla, lead_sla.name)
+ # check if SLA is removed if condition fails
+ lead.reload()
+ lead.source = None
+ lead.save()
+ applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
+ self.assertFalse(applied_sla)
+
def tearDown(self):
for d in frappe.get_all("Service Level Agreement"):
frappe.delete_doc("Service Level Agreement", d.name, force=1)
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index fbf2594..bc9f04e 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -125,17 +125,23 @@
if default_filters is None:
default_filters = {}
+ test_filters = []
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
report_filters = frappe._dict(default_filters).copy().update(filters)
- report_data = report_execute_fn(report_filters)
+ test_filters.append(report_filters)
if optional_filters:
for key, value in optional_filters.items():
- filter_with_optional_param = report_filters.copy().update({key: value})
- report_execute_fn(filter_with_optional_param)
+ test_filters.append(report_filters.copy().update({key: value}))
- return report_data
+ for test_filter in test_filters:
+ try:
+ report_execute_fn(test_filter)
+ except Exception:
+ print(f"Report failed to execute with filters: {test_filter}")
+ raise
+
def timeout(seconds=30, error_message="Test timed out."):
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 1d8b3a8..feea228 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -181,8 +181,6 @@
if len(child_table_values) > 1:
self.set(default_field, None)
- else:
- self.set(default_field, list(child_table_values)[0])
def delete_events(ref_type, ref_name):
events = frappe.db.sql_list(""" SELECT
diff --git a/requirements.txt b/requirements.txt
index faefb77..f447fac 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
# frappe # https://github.com/frappe/frappe is installed during bench-init
gocardless-pro~=1.22.0
-googlemaps # used in ERPNext, but dependency is defined in Frappe
+googlemaps
pandas~=1.1.5
plaid-python~=7.2.1
pycountry~=20.7.3