diff --git a/.github/helper/semgrep_rules/README.md b/.github/helper/semgrep_rules/README.md
deleted file mode 100644
index 670d8d280f2..00000000000
--- a/.github/helper/semgrep_rules/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Semgrep linting
-
-## What is semgrep?
-Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
-
-Example:
-
-To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
-
-You can read more such examples in `.github/helper/semgrep_rules` directory.
-
-# Why/when to use this?
-We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
-
-## Running locally
-
-Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
-
-To run locally use following command:
-
-`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
-
-## Testing
-semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
-
-When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
-
-To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
-
-
-## Reference
-
-If you are new to Semgrep read following pages to get started on writing/modifying rules:
-
-- https://semgrep.dev/docs/getting-started/
-- https://semgrep.dev/docs/writing-rules/rule-syntax
-- https://semgrep.dev/docs/writing-rules/pattern-examples/
-- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases
diff --git a/.github/helper/semgrep_rules/report.yml b/.github/helper/semgrep_rules/report.yml
deleted file mode 100644
index f2a9b167399..00000000000
--- a/.github/helper/semgrep_rules/report.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-rules:
-- id: frappe-missing-translate-function-in-report-python
- paths:
- include:
- - "**/report"
- exclude:
- - "**/regional"
- pattern-either:
- - patterns:
- - pattern: |
- {..., "label": "...", ...}
- - pattern-not: |
- {..., "label": _("..."), ...}
- - patterns:
- - pattern: dict(..., label="...", ...)
- - pattern-not: dict(..., label=_("..."), ...)
- message: |
- All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translated-values-in-business-logic
- paths:
- include:
- - "**/report"
- patterns:
- - pattern-inside: |
- {..., filters: [...], ...}
- - pattern: |
- {..., options: [..., __("..."), ...], ...}
- message: |
- Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
- languages: [javascript]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/security.py b/.github/helper/semgrep_rules/security.py
deleted file mode 100644
index f477d7c1768..00000000000
--- a/.github/helper/semgrep_rules/security.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def function_name(input):
- # ruleid: frappe-codeinjection-eval
- eval(input)
-
-# ok: frappe-codeinjection-eval
-eval("1 + 1")
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
deleted file mode 100644
index 8b219792080..00000000000
--- a/.github/helper/semgrep_rules/security.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-rules:
-- id: frappe-codeinjection-eval
- patterns:
- - pattern-not: eval("...")
- - pattern: eval(...)
- message: |
- Detected the use of eval(). eval() can be dangerous if used to evaluate
- dynamic content. Avoid it or use safe_eval().
- languages: [python]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
deleted file mode 100644
index 9cdfb75d0be..00000000000
--- a/.github/helper/semgrep_rules/translate.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// ruleid: frappe-translation-empty-string
-__("")
-// ruleid: frappe-translation-empty-string
-__('')
-
-// ok: frappe-translation-js-formatting
-__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
-
-// ruleid: frappe-translation-js-formatting
-__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
-
-// ok: frappe-translation-js-formatting
-__('This is fine');
-
-
-// ok: frappe-translation-trailing-spaces
-__('This is fine');
-
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__('this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok');
-
-// ok: frappe-translation-js-splitting
-__('You have {0} subscribers in your mailing list.', [subscribers.length])
-
-// todoruleid: frappe-translation-js-splitting
-__('You have') + subscribers.length + __('subscribers in your mailing list.')
-
-// ruleid: frappe-translation-js-splitting
-__('You have' + 'subscribers in your mailing list.')
-
-// ruleid: frappe-translation-js-splitting
-__('You have {0} subscribers' +
- 'in your mailing list', [subscribers.length])
-
-// ok: frappe-translation-js-splitting
-__("Ctrl+Enter to add comment")
-
-// ruleid: frappe-translation-js-splitting
-__('You have {0} subscribers \
- in your mailing list', [subscribers.length])
diff --git a/.github/helper/semgrep_rules/translate.py b/.github/helper/semgrep_rules/translate.py
deleted file mode 100644
index 9de6aa94f01..00000000000
--- a/.github/helper/semgrep_rules/translate.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Examples taken from https://frappeframework.com/docs/user/en/translations
-# This file is used for testing the tests.
-
-from frappe import _
-
-full_name = "Jon Doe"
-# ok: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
-# ruleid: frappe-translation-python-formatting
-_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
-
-
-subscribers = ["Jon", "Doe"]
-# ok: frappe-translation-python-formatting
-_('You have {0} subscribers in your mailing list.').format(len(subscribers))
-
-# ruleid: frappe-translation-python-splitting
-_('You have') + len(subscribers) + _('subscribers in your mailing list.')
-
-# ruleid: frappe-translation-python-splitting
-_('You have {0} subscribers \
- in your mailing list').format(len(subscribers))
-
-# ok: frappe-translation-python-splitting
-_('You have {0} subscribers') \
- + 'in your mailing list'
-
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _("You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice")
-
-# ok: frappe-translation-trailing-spaces
-msg = ' ' + _("You have {0} pending invoices") + ' '
-
-# ruleid: frappe-translation-python-formatting
-_(f"can not format like this - {subscribers}")
-# ruleid: frappe-translation-python-splitting
-_(f"what" + f"this is also not cool")
-
-
-# ruleid: frappe-translation-empty-string
-_("")
-# ruleid: frappe-translation-empty-string
-_('')
-
-
-class Test:
- # ok: frappe-translation-python-splitting
- def __init__(
- args
- ):
- pass
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
deleted file mode 100644
index 5f03fb9fd00..00000000000
--- a/.github/helper/semgrep_rules/translate.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-rules:
-- id: frappe-translation-empty-string
- pattern-either:
- - pattern: _("")
- - pattern: __("")
- message: |
- Empty string is useless for translation.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-trailing-spaces
- pattern-either:
- - pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- - pattern: __("=~/(^[ \t]+|[ \t]+$)/")
- message: |
- Trailing or leading whitespace not allowed in translate strings.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-formatting
- pattern-either:
- - pattern: _("..." % ...)
- - pattern: _("...".format(...))
- - pattern: _(f"...")
- message: |
- Only positional formatters are allowed and formatting should not be done before translating.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-formatting
- patterns:
- - pattern: __(`...`)
- - pattern-not: __("...")
- message: |
- Template strings are not allowed for text formatting.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-splitting
- pattern-either:
- - pattern: _(...) + _(...)
- - pattern: _("..." + "...")
- - pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
- - pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
- message: |
- Do not split strings inside translate function. Do not concatenate using translate functions.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-splitting
- pattern-either:
- - pattern-regex: '__\([^\)]*[\\]\s+'
- - pattern: __('...' + '...', ...)
- - pattern: __('...') + __('...')
- message: |
- Do not split strings inside translate function. Do not concatenate using translate functions.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/ux.js b/.github/helper/semgrep_rules/ux.js
deleted file mode 100644
index ae73f9cc603..00000000000
--- a/.github/helper/semgrep_rules/ux.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-// ok: frappe-missing-translate-function-js
-frappe.msgprint('{{ _("Both login and password required") }}');
-
-// ruleid: frappe-missing-translate-function-js
-frappe.msgprint('What');
-
-// ok: frappe-missing-translate-function-js
-frappe.throw(' {{ _("Both login and password required") }}. ');
diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml
deleted file mode 100644
index dd667f36c0f..00000000000
--- a/.github/helper/semgrep_rules/ux.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-rules:
-- id: frappe-missing-translate-function-python
- pattern-either:
- - patterns:
- - pattern: frappe.msgprint("...", ...)
- - pattern-not: frappe.msgprint(_("..."), ...)
- - patterns:
- - pattern: frappe.throw("...", ...)
- - pattern-not: frappe.throw(_("..."), ...)
- message: |
- All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-missing-translate-function-js
- pattern-either:
- - patterns:
- - pattern: frappe.msgprint("...", ...)
- - pattern-not: frappe.msgprint(__("..."), ...)
- # ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- - pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- - patterns:
- - pattern: frappe.throw("...", ...)
- - pattern-not: frappe.throw(__("..."), ...)
- # ignore microtemplating
- - pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
- message: |
- All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [javascript]
- severity: ERROR
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index c2363397c47..ebb88c9edac 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -10,13 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - uses: returntocorp/semgrep-action@v1
- env:
- SEMGREP_TIMEOUT: 120
- with:
- config: >-
- r/python.lang.correctness
- .github/helper/semgrep_rules
- name: Set up Python 3.8
uses: actions/setup-python@v2
@@ -24,4 +17,15 @@ jobs:
python-version: 3.8
- name: Install and Run Pre-commit
- uses: pre-commit/action@v2.0.0
+ uses: pre-commit/action@v2.0.3
+
+ - name: Download Semgrep rules
+ run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
+
+ - uses: returntocorp/semgrep-action@v1
+ env:
+ SEMGREP_TIMEOUT: 120
+ with:
+ config: >-
+ r/python.lang.correctness
+ ./frappe-semgrep-rules/rules
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 05caafe1c47..3596c340175 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -81,7 +81,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child):
if child.get("is_group"):
is_group = child.get("is_group")
- elif len(set(child.keys()) - set(["account_type", "root_type", "is_group", "tax_rate", "account_number"])):
+ elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
is_group = 1
else:
is_group = 0
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 6f362c1fbb9..ee2e319a6fc 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -27,10 +27,12 @@
"payment_accounts_section",
"party_balance",
"paid_from",
+ "paid_from_account_type",
"paid_from_account_currency",
"paid_from_account_balance",
"column_break_18",
"paid_to",
+ "paid_to_account_type",
"paid_to_account_currency",
"paid_to_account_balance",
"payment_amounts_section",
@@ -440,7 +442,8 @@
"depends_on": "eval:(doc.paid_from && doc.paid_to)",
"fieldname": "reference_no",
"fieldtype": "Data",
- "label": "Cheque/Reference No"
+ "label": "Cheque/Reference No",
+ "mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
},
{
"fieldname": "column_break_23",
@@ -452,6 +455,7 @@
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Cheque/Reference Date",
+ "mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')",
"search_index": 1
},
{
@@ -707,15 +711,30 @@
"label": "Received Amount After Tax (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "paid_from.account_type",
+ "fieldname": "paid_from_account_type",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Paid From Account Type"
+ },
+ {
+ "fetch_from": "paid_to.account_type",
+ "fieldname": "paid_to_account_type",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Paid To Account Type"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-07-09 08:58:15.008761",
+ "modified": "2021-10-22 17:50:24.632806",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index aa373bc2fcc..9074defa577 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -10,6 +10,9 @@ frappe.ui.form.on('Payment Order', {
}
}
});
+
+ frm.set_df_property('references', 'cannot_add_rows', true);
+ frm.set_df_property('references', 'cannot_delete_rows', true);
},
refresh: function(frm) {
if (frm.doc.docstatus == 0) {
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 4d6e4a2ba07..d6e35c6a50d 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -180,8 +180,7 @@
"fieldname": "pos_transactions",
"fieldtype": "Table",
"label": "POS Transactions",
- "options": "POS Invoice Reference",
- "reqd": 1
+ "options": "POS Invoice Reference"
},
{
"fieldname": "pos_opening_entry",
@@ -229,7 +228,7 @@
"link_fieldname": "pos_closing_entry"
}
],
- "modified": "2021-05-05 16:59:49.723261",
+ "modified": "2021-10-20 16:19:25.340565",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index 9dae3a7b75e..28bd10283e7 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -114,6 +114,8 @@ class POSInvoiceMergeLog(Document):
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
+ rounding_adjustment, base_rounding_adjustment = 0, 0
+ rounded_total, base_rounded_total = 0, 0
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
@@ -162,6 +164,11 @@ class POSInvoiceMergeLog(Document):
found = True
if not found:
payments.append(payment)
+ rounding_adjustment += doc.rounding_adjustment
+ rounded_total += doc.rounded_total
+ base_rounding_adjustment += doc.rounding_adjustment
+ base_rounded_total += doc.rounded_total
+
if loyalty_points_sum:
invoice.redeem_loyalty_points = 1
@@ -171,6 +178,10 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items)
invoice.set('payments', payments)
invoice.set('taxes', taxes)
+ invoice.set('rounding_adjustment',rounding_adjustment)
+ invoice.set('rounding_adjustment',base_rounding_adjustment)
+ invoice.set('base_rounded_total',base_rounded_total)
+ invoice.set('rounded_total',rounded_total)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
@@ -246,7 +257,10 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
- invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
+ invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions'))
+ if frappe.flags.in_test and not invoices:
+ invoices = get_all_unconsolidated_invoices()
+
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 10 and closing_entry:
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 8afa0abd36c..9c9f37bba27 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -120,6 +120,7 @@
{
"fieldname": "payments",
"fieldtype": "Table",
+ "label": "Payment Methods",
"options": "POS Payment Method",
"reqd": 1
},
@@ -377,7 +378,7 @@
"link_fieldname": "pos_profile"
}
],
- "modified": "2021-02-01 13:52:51.081311",
+ "modified": "2021-10-14 14:17:00.469298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 0637fdaef02..ef44b414761 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -29,6 +29,9 @@ def get_pricing_rules(args, doc=None):
pricing_rules = []
values = {}
+ if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}):
+ return
+
for apply_on in ['Item Code', 'Item Group', 'Brand']:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index aef9243aad0..39bb3cdca6a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -590,5 +590,11 @@ frappe.ui.form.on("Purchase Invoice", {
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+
+ if (frm.doc.company) {
+ frappe.db.get_value('Company', frm.doc.company, 'default_payable_account', (r) => {
+ frm.set_value('credit_to', r.default_payable_account);
+ });
+ }
},
})
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 24928186b6b..9522c01f33a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -10,9 +10,17 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.setup_posting_date_time_check();
this._super(doc);
},
+
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ let me = this;
+ if (this.frm.doc.company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_receivable_account', (r) => {
+ me.frm.set_value('debit_to', r.default_receivable_account);
+ });
+ }
},
+
onload: function() {
var me = this;
this._super();
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 902fceeac75..aeab2e01c14 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -2026,22 +2026,23 @@ def update_multi_mode_option(doc, pos_profile):
def append_payment(payment_mode):
payment = doc.append('payments', {})
payment.default = payment_mode.default
- payment.mode_of_payment = payment_mode.parent
+ payment.mode_of_payment = payment_mode.mop
payment.account = payment_mode.default_account
payment.type = payment_mode.type
doc.set('payments', [])
invalid_modes = []
- for pos_payment_method in pos_profile.get('payments'):
- pos_payment_method = pos_payment_method.as_dict()
+ mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')]
+ mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
- payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
+ for row in pos_profile.get('payments'):
+ payment_mode = mode_of_payments_info.get(row.mode_of_payment)
if not payment_mode:
- invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
+ invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
continue
- payment_mode[0].default = pos_payment_method.default
- append_payment(payment_mode[0])
+ payment_mode.default = row.default
+ append_payment(payment_mode)
if invalid_modes:
if invalid_modes == 1:
@@ -2057,6 +2058,24 @@ def get_all_mode_of_payments(doc):
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=1)
+def get_mode_of_payments_info(mode_of_payments, company):
+ data = frappe.db.sql(
+ """
+ select
+ mpa.default_account, mpa.parent as mop, mp.type as type
+ from
+ `tabMode of Payment Account` mpa,`tabMode of Payment` mp
+ where
+ mpa.parent = mp.name and
+ mpa.company = %s and
+ mp.enabled = 1 and
+ mp.name in (%s)
+ group by
+ mp.name
+ """, (company, mode_of_payments), as_dict=1)
+
+ return {row.get('mop'): row for row in data}
+
def get_mode_of_payment_info(mode_of_payment, company):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index c3cb8396d0d..54042fbe748 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -165,6 +165,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{
'pan_no': pan_no,
+ 'tax_withholding_category': tax_details.tax_withholding_category,
'valid_from': ('>=', tax_details.from_date),
'valid_upto': ('<=', tax_details.to_date)
}, 'name')
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index 7643eca7635..e6580493095 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -68,7 +68,7 @@
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
-
{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}
+
+ {%- if einvoice.DispDtls -%}
+ {%- set dispatch = einvoice.DispDtls -%}
+ {{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}
+ {% endif %}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index a600ead9e54..0475231a934 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -114,8 +114,9 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
# opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]:
- account_name = get_root_account_name(data[0].root_type, company)
- opening_value += get_opening_balance(account_name, data, company)
+ if data:
+ account_name = get_root_account_name(data[0].root_type, company)
+ opening_value += (get_opening_balance(account_name, data, company) or 0.0)
opening_balance[company] = opening_value
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 0094bc2eebe..31416da4ac4 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -155,6 +155,8 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("group_by") == "Group by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no"
+ if filters.get("group_by") == "Group by Account":
+ order_by_statement = "order by account, posting_date, creation"
if filters.get("include_default_book_entries"):
filters['company_fb'] = frappe.db.get_value("Company",
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 621b697aca4..6a7f2e5b535 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -44,16 +44,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
if rate and tds_deducted:
row = {
- 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
- 'supplier': supplier_map.get(supplier).name
+ 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
+ 'supplier': supplier_map.get(supplier, {}).get('name')
}
if filters.naming_series == 'Naming Series':
- row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
+ row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')})
row.update({
'section_code': tax_withholding_category,
- 'entity_type': supplier_map.get(supplier).supplier_type,
+ 'entity_type': supplier_map.get(supplier, {}).get('supplier_type'),
'tds_rate': rate,
'total_amount_credited': total_amount_credited,
'tds_deducted': tds_deducted,
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index 7e3ecaf3ab6..236e47594d1 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -454,6 +454,17 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "KSA VAT Report",
+ "link_to": "KSA VAT",
+ "link_type": "Report",
+ "onboard": 0,
+ "only_for": "Saudi Arabia",
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 0,
@@ -1034,6 +1045,16 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "KSA VAT Setting",
+ "link_to": "KSA VAT Setting",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "Saudi Arabia",
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 0,
@@ -1082,7 +1103,7 @@
"type": "Link"
}
],
- "modified": "2021-08-23 16:06:34.167267",
+ "modified": "2021-08-26 13:15:52.872470",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
index 75f42a9f783..06989a95da7 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
@@ -16,9 +16,8 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldname:"status",
label: __("Status"),
fieldtype: "Select",
- options: "In Location\nDisposed",
- default: 'In Location',
- reqd: 1
+ options: "\nIn Location\nDisposed",
+ default: 'In Location'
},
{
"fieldname":"filter_based_on",
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index e370b9d0cb3..63685fef465 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -45,12 +45,13 @@ def get_conditions(filters):
if filters.get('cost_center'):
conditions["cost_center"] = filters.get('cost_center')
- # In Store assets are those that are not sold or scrapped
- operand = 'not in'
- if status not in 'In Location':
- operand = 'in'
+ if status:
+ # In Store assets are those that are not sold or scrapped
+ operand = 'not in'
+ if status not in 'In Location':
+ operand = 'in'
- conditions['status'] = (operand, ['Sold', 'Scrapped'])
+ conditions['status'] = (operand, ['Sold', 'Scrapped'])
return conditions
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 829da8953bf..45574a6ba19 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -820,6 +820,38 @@ class AccountsController(TransactionBase):
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
unlink_ref_doc_from_payment_entries(self)
+ if self.doctype == "Sales Order":
+ self.unlink_ref_doc_from_po()
+
+ def unlink_ref_doc_from_po(self):
+ so_items = []
+ for item in self.items:
+ so_items.append(item.name)
+
+ linked_po = list(set(frappe.get_all(
+ 'Purchase Order Item',
+ filters = {
+ 'sales_order': self.name,
+ 'sales_order_item': ['in', so_items],
+ 'docstatus': ['<', 2]
+ },
+ pluck='parent'
+ )))
+
+ if linked_po:
+ frappe.db.set_value(
+ 'Purchase Order Item', {
+ 'sales_order': self.name,
+ 'sales_order_item': ['in', so_items],
+ 'docstatus': ['<', 2]
+ },{
+ 'sales_order': None,
+ 'sales_order_item': None
+ }
+ )
+
+ frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po)))
+
def get_tax_map(self):
tax_map = {}
for tax in self.get('taxes'):
@@ -1037,7 +1069,7 @@ class AccountsController(TransactionBase):
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
- .format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
+ .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
@@ -1359,8 +1391,8 @@ class AccountsController(TransactionBase):
total = 0
base_total = 0
for d in self.get("payment_schedule"):
- total += flt(d.payment_amount)
- base_total += flt(d.base_payment_amount)
+ total += flt(d.payment_amount, d.precision("payment_amount"))
+ base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
@@ -1376,8 +1408,9 @@ class AccountsController(TransactionBase):
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
- if total != flt(grand_total, self.precision("grand_total")) or \
- base_total != flt(base_grand_total, self.precision("base_grand_total")):
+
+ if flt(total, self.precision("grand_total")) != flt(grand_total, self.precision("grand_total")) or \
+ flt(base_total, self.precision("base_grand_total")) != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self):
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 9f28646a0b9..eab7a07be90 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -132,7 +132,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {field} from `tabSupplier`
where docstatus < 2
and ({key} like %(txt)s
- or supplier_name like %(txt)s) and disabled=0
+ or supplier_name like %(txt)s) and disabled=0
+ and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
{mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
@@ -565,7 +566,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(['name', query_selector, dimensions])
- output = frappe.get_all(doctype, filters=query_filters)
+ output = frappe.get_list(doctype, filters=query_filters)
result = [d.name for d in output]
return [(d,) for d in set(result)]
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 8738204ce09..49a76da6976 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -216,11 +216,14 @@ class StatusUpdater(Document):
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
- if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
+ if overflow_percent - allowance > 0.01:
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed']
- self.limits_crossed_error(args, item, qty_or_amount)
+ if role not in frappe.get_roles():
+ self.limits_crossed_error(args, item, qty_or_amount)
+ else:
+ self.warn_about_bypassing_with_role(item, qty_or_amount, role)
def limits_crossed_error(self, args, item, qty_or_amount):
'''Raise exception for limits crossed'''
@@ -238,6 +241,19 @@ class StatusUpdater(Document):
frappe.bold(item.get('item_code'))
) + '
' + action_msg, OverAllowanceError, title = _('Limit Crossed'))
+ def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
+ action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
+
+ msg = (_("{} of {} {} ignored for item {} because you have {} role.")
+ .format(
+ action,
+ _(item["target_ref_field"].title()),
+ frappe.bold(item["reduce_by"]),
+ frappe.bold(item.get('item_code')),
+ role)
+ )
+ frappe.msgprint(msg, indicator="orange", alert=True)
+
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index fbfdfdfac89..1c57a56371b 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -260,7 +260,9 @@ class calculate_taxes_and_totals(object):
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
def calculate_taxes(self):
- self.doc.rounding_adjustment = 0
+ if not self.doc.get('is_consolidated'):
+ self.doc.rounding_adjustment = 0
+
# maintain actual tax rate based on idx
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
for tax in self.doc.get("taxes") if tax.charge_type == "Actual"])
@@ -312,7 +314,9 @@ class calculate_taxes_and_totals(object):
# adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
- and self.doc.discount_amount and self.doc.apply_discount_on == "Grand Total":
+ and self.doc.discount_amount \
+ and self.doc.apply_discount_on == "Grand Total" \
+ and not self.doc.get('is_consolidated'):
self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"))
@@ -405,11 +409,16 @@ class calculate_taxes_and_totals(object):
self.doc.rounding_adjustment = diff
def calculate_totals(self):
- self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) \
- if self.doc.get("taxes") else flt(self.doc.net_total)
+ if self.doc.get("taxes"):
+ self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
+ else:
+ self.doc.grand_total = flt(self.doc.net_total)
- self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
+ if self.doc.get("taxes"):
+ self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
- flt(self.doc.rounding_adjustment), self.doc.precision("total_taxes_and_charges"))
+ else:
+ self.doc.total_taxes_and_charges = 0.0
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
@@ -446,19 +455,20 @@ class calculate_taxes_and_totals(object):
self.doc.total_net_weight += d.total_weight
def set_rounded_total(self):
- if self.doc.meta.get_field("rounded_total"):
- if self.doc.is_rounded_total_disabled():
- self.doc.rounded_total = self.doc.base_rounded_total = 0
- return
+ if not self.doc.get('is_consolidated'):
+ if self.doc.meta.get_field("rounded_total"):
+ if self.doc.is_rounded_total_disabled():
+ self.doc.rounded_total = self.doc.base_rounded_total = 0
+ return
- self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
- self.doc.currency, self.doc.precision("rounded_total"))
+ self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
+ self.doc.currency, self.doc.precision("rounded_total"))
- #if print_in_rate is set, we would have already calculated rounding adjustment
- self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
- self.doc.precision("rounding_adjustment"))
+ #if print_in_rate is set, we would have already calculated rounding adjustment
+ self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
+ self.doc.precision("rounding_adjustment"))
- self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
+ self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
if not self.doc.get('is_consolidated'):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 8027cbc69cc..a304171ed49 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -305,6 +305,8 @@ def make_request_for_quotation(source_name, target_doc=None):
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
def set_missing_values(source, target):
+ target.opportunity_name = source.name
+
if source.opportunity_from == "Lead":
target.lead_name = source.party_name
diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
index f9e3b6ae32e..6d44b2cb977 100644
--- a/erpnext/e_commerce/product_data_engine/filters.py
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
-from frappe import _dict
from frappe.utils import floor
@@ -96,38 +95,32 @@ class ProductFiltersBuilder:
return
attributes = [row.attribute for row in self.doc.filter_attributes]
- attribute_docs = [
- frappe.get_doc('Item Attribute', attribute) for attribute in attributes
- ]
- valid_attributes = []
+ if not attributes:
+ return []
- for attr_doc in attribute_docs:
- selected_attributes = []
- for attr in attr_doc.item_attribute_values:
- or_filters = []
- filters= [
- ["Item Variant Attribute", "attribute", "=", attr.parent],
- ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
- ]
- if self.item_group:
- or_filters.extend([
- ["item_group", "=", self.item_group],
- ["Website Item Group", "item_group", "=", self.item_group]
- ])
+ result = frappe.db.sql(
+ """
+ select
+ distinct attribute, attribute_value
+ from
+ `tabItem Variant Attribute`
+ where
+ attribute in %(attributes)s
+ and attribute_value is not null
+ """,
+ {"attributes": attributes},
+ as_dict=1,
+ )
- if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
- selected_attributes.append(attr)
+ attribute_value_map = {}
+ for d in result:
+ attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
- if selected_attributes:
- valid_attributes.append(
- _dict(
- item_attribute_values=selected_attributes,
- name=attr_doc.name
- )
- )
-
- return valid_attributes
+ out = []
+ for name, values in attribute_value_map.items():
+ out.append(frappe._dict(name=name, item_attribute_values=values))
+ return out
def get_discount_filters(self, discounts):
discount_filters = []
@@ -147,4 +140,4 @@ class ProductFiltersBuilder:
label = f"{discount}% and below"
discount_filters.append([discount, label])
- return discount_filters
\ No newline at end of file
+ return discount_filters
diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
index 925e6e7be3c..b52e140fcc4 100644
--- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
+++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
@@ -175,9 +175,7 @@ class TestProductDataEngine(unittest.TestCase):
filter_engine = ProductFiltersBuilder()
attribute_filter = filter_engine.get_attribute_filters()[0]
- attributes = attribute_filter.item_attribute_values
-
- attribute_values = [d.attribute_value for d in attributes]
+ attribute_values = attribute_filter.item_attribute_values
self.assertEqual(attribute_filter.name, "Test Size")
self.assertGreater(len(attribute_values), 0)
@@ -349,4 +347,4 @@ def create_variant_web_item():
variant.save()
if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
- make_website_item(variant, save=True)
\ No newline at end of file
+ make_website_item(variant, save=True)
diff --git a/erpnext/e_commerce/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py
index 51398596fd8..0cc0ab7c002 100644
--- a/erpnext/e_commerce/shopping_cart/utils.py
+++ b/erpnext/e_commerce/shopping_cart/utils.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-
import frappe
import frappe.defaults
@@ -17,10 +14,19 @@ def show_cart_count():
return False
def set_cart_count(login_manager):
- role, parties = check_customer_or_supplier()
- if role == 'Supplier': return
+ # since this is run only on hooks login event
+ # make sure user is already a customer
+ # before trying to set cart count
+ user_is_customer = is_customer()
+ if not user_is_customer:
+ return
+
if show_cart_count():
from erpnext.e_commerce.shopping_cart.cart import set_cart_count
+
+ # set_cart_count will try to fetch existing cart quotation
+ # or create one if non existent (and create a customer too)
+ # cart count is calculated from this quotation's items
set_cart_count()
def clear_cart_count(login_manager):
@@ -31,13 +37,13 @@ def update_website_context(context):
cart_enabled = is_cart_enabled()
context["shopping_cart_enabled"] = cart_enabled
-def check_customer_or_supplier():
- if frappe.session.user:
+def is_customer():
+ if frappe.session.user and frappe.session.user != "Guest":
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
for link in contact.links:
- if link.link_doctype in ('Customer', 'Supplier'):
- return link.link_doctype, link.link_name
+ if link.link_doctype == 'Customer':
+ return True
- return 'Customer', None
+ return False
diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
similarity index 100%
rename from erpnext/stock/report/process_loss_report/__init__.py
rename to erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
new file mode 100644
index 00000000000..d4d4a512b58
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-11 05:09:53.773838",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "region",
+ "region_code",
+ "country",
+ "country_code"
+ ],
+ "fields": [
+ {
+ "fieldname": "region",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Region"
+ },
+ {
+ "fieldname": "region_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Region Code"
+ },
+ {
+ "fieldname": "country",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Country"
+ },
+ {
+ "fieldname": "country_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Country Code"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-14 05:33:06.444710",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "TaxJar Nexus",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
new file mode 100644
index 00000000000..c24aa8ca7d4
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TaxJarNexus(Document):
+ pass
diff --git a/erpnext/regional/united_states/product_tax_category_data.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
similarity index 100%
rename from erpnext/regional/united_states/product_tax_category_data.json
rename to erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
index 62d5709f51f..d49598932fe 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
@@ -5,5 +5,16 @@ frappe.ui.form.on('TaxJar Settings', {
is_sandbox: (frm) => {
frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox);
- }
+ },
+
+ refresh: (frm) => {
+ frm.add_custom_button(__('Update Nexus List'), function() {
+ frm.call({
+ doc: frm.doc,
+ method: 'update_nexus_list'
+ });
+ });
+ },
+
+
});
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index c0d60f7a317..2d17f2ed832 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -6,8 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "is_sandbox",
"taxjar_calculate_tax",
+ "is_sandbox",
"taxjar_create_transactions",
"credentials",
"api_key",
@@ -16,7 +16,10 @@
"configuration",
"tax_account_head",
"configuration_cb",
- "shipping_account_head"
+ "shipping_account_head",
+ "section_break_12",
+ "nexus_address",
+ "nexus"
],
"fields": [
{
@@ -54,6 +57,7 @@
},
{
"default": "0",
+ "depends_on": "taxjar_calculate_tax",
"fieldname": "is_sandbox",
"fieldtype": "Check",
"label": "Sandbox Mode"
@@ -69,6 +73,7 @@
},
{
"default": "0",
+ "depends_on": "taxjar_calculate_tax",
"fieldname": "taxjar_create_transactions",
"fieldtype": "Check",
"label": "Create TaxJar Transaction"
@@ -82,11 +87,28 @@
{
"fieldname": "cb_keys",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break",
+ "label": "Nexus List"
+ },
+ {
+ "fieldname": "nexus_address",
+ "fieldtype": "HTML",
+ "label": "Nexus Address"
+ },
+ {
+ "fieldname": "nexus",
+ "fieldtype": "Table",
+ "label": "Nexus",
+ "options": "TaxJar Nexus",
+ "read_only": 1
}
],
"issingle": 1,
"links": [],
- "modified": "2020-04-30 04:38:03.311089",
+ "modified": "2021-10-06 10:59:13.475442",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index 9dd481747ec..f430a9e9bae 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -4,9 +4,98 @@
from __future__ import unicode_literals
-# import frappe
+import json
+import os
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
+from frappe.permissions import add_permission, update_permission_property
+
+from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document):
- pass
+
+ def on_update(self):
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+ TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+ fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
+ fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
+
+ if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
+ if not fields_already_exist:
+ add_product_tax_categories()
+ make_custom_fields()
+ add_permissions()
+ frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
+
+ elif fields_already_exist and fields_hidden:
+ toggle_tax_category_fields(hidden='0')
+
+ elif fields_already_exist:
+ toggle_tax_category_fields(hidden='1')
+
+ def validate(self):
+ self.calculate_taxes_validation_for_create_transactions()
+
+ @frappe.whitelist()
+ def update_nexus_list(self):
+ client = get_client()
+ nexus = client.nexus_regions()
+
+ new_nexus_list = [frappe._dict(address) for address in nexus]
+
+ self.set('nexus', [])
+ self.set('nexus', new_nexus_list)
+ self.save()
+
+ def calculate_taxes_validation_for_create_transactions(self):
+ if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
+ frappe.throw(frappe._('Before enabling
Create Transaction or
Sandbox Mode , you need to check the
Enable Tax Calculation box'))
+
+
+def toggle_tax_category_fields(hidden):
+ frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+ frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+
+
+def add_product_tax_categories():
+ with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
+ tax_categories = json.loads(f.read())
+ create_tax_categories(tax_categories['categories'])
+
+def create_tax_categories(data):
+ for d in data:
+ if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
+ tax_category = frappe.new_doc('Product Tax Category')
+ tax_category.description = d.get("description")
+ tax_category.product_tax_code = d.get("product_tax_code")
+ tax_category.category_name = d.get("name")
+ tax_category.db_insert()
+
+def make_custom_fields(update=True):
+ custom_fields = {
+ 'Sales Invoice Item': [
+ dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
+ label='Product Tax Category', fetch_from='item_code.product_tax_category'),
+ dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
+ label='Tax Collectable', read_only=1),
+ dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
+ label='Taxable Amount', read_only=1)
+ ],
+ 'Item': [
+ dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
+ label='Product Tax Category')
+ ]
+ }
+ create_custom_fields(custom_fields, update=update)
+
+def add_permissions():
+ doctype = "Product Tax Category"
+ for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
+ add_permission(doctype, role, 0)
+ update_permission_property(doctype, role, 0, 'write', 1)
+ update_permission_property(doctype, role, 0, 'create', 1)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index 870a4ef54cc..2a7243c2430 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -4,7 +4,7 @@ import frappe
import taxjar
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
-from frappe.utils import cint
+from frappe.utils import cint, flt
from erpnext import get_default_company
@@ -103,7 +103,7 @@ def get_tax_data(doc):
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
- line_items = [get_line_item_dict(item) for item in doc.items]
+ line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
@@ -139,14 +139,21 @@ def get_state_code(address, location):
return state_code
-def get_line_item_dict(item):
- return dict(
+def get_line_item_dict(item, docstatus):
+ tax_dict = dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
+ if docstatus == 1:
+ tax_dict.update({
+ 'sales_tax':item.get('tax_collectable')
+ })
+
+ return tax_dict
+
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
return
@@ -164,6 +171,9 @@ def set_sales_tax(doc, method):
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
return
+ # check if delivering within a nexus
+ check_for_nexus(doc, tax_dict)
+
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
@@ -191,6 +201,17 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals")
+def check_for_nexus(doc, tax_dict):
+ if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
+ for item in doc.get("items"):
+ item.tax_collectable = flt(0)
+ item.taxable_amount = flt(0)
+
+ for tax in doc.taxes:
+ if tax.account_head == TAX_ACCOUNT_HEAD:
+ doc.taxes.remove(tax)
+ return
+
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3094deaafa7..6be3e25440a 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -257,6 +257,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
+ "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
@@ -266,7 +267,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
- "on_trash": "erpnext.regional.check_deletion_permission",
+ "on_trash": [
+ "erpnext.regional.check_deletion_permission",
+ "erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
+ ],
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 216d8f6bb3a..559bd393e62 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -156,6 +156,8 @@ def get_employees_having_an_event_today(event_type):
DAY({condition_column}) = DAY(%(today)s)
AND
MONTH({condition_column}) = MONTH(%(today)s)
+ AND
+ YEAR({condition_column}) < YEAR(%(today)s)
AND
`status` = 'Active'
""",
@@ -166,6 +168,8 @@ def get_employees_having_an_event_today(event_type):
DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
AND
DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
+ AND
+ DATE_PART('year', {condition_column}) < date_part('year', %(today)s)
AND
"status" = 'Active'
""",
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8d6dfa2c1d2..8a2da0866e9 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs):
"email": user,
"first_name": user,
"new_password": "password",
+ "send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert()
diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
index 164d48b8952..b05175200e9 100644
--- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
-from erpnext.hr.utils import update_employee, validate_active_employee
+from erpnext.hr.utils import update_employee_work_history, validate_active_employee
class EmployeePromotion(Document):
@@ -23,10 +23,10 @@ class EmployeePromotion(Document):
def on_submit(self):
employee = frappe.get_doc("Employee", self.employee)
- employee = update_employee(employee, self.promotion_details, date=self.promotion_date)
+ employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date)
employee.save()
def on_cancel(self):
employee = frappe.get_doc("Employee", self.employee)
- employee = update_employee(employee, self.promotion_details, cancel=True)
+ employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
employee.save()
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index b1f66098f0d..29d93f348cc 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
-from erpnext.hr.utils import update_employee
+from erpnext.hr.utils import update_employee_work_history
class EmployeeTransfer(Document):
@@ -24,7 +24,7 @@ class EmployeeTransfer(Document):
new_employee = frappe.copy_doc(employee)
new_employee.name = None
new_employee.employee_number = None
- new_employee = update_employee(new_employee, self.transfer_details, date=self.transfer_date)
+ new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
new_employee.internal_work_history = []
new_employee.date_of_joining = self.transfer_date
@@ -39,7 +39,7 @@ class EmployeeTransfer(Document):
employee.db_set("relieving_date", self.transfer_date)
employee.db_set("status", "Left")
else:
- employee = update_employee(employee, self.transfer_details, date=self.transfer_date)
+ employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
employee.company = self.new_company
employee.date_of_joining = self.transfer_date
@@ -56,7 +56,7 @@ class EmployeeTransfer(Document):
employee.status = "Active"
employee.relieving_date = ''
else:
- employee = update_employee(employee, self.transfer_details, cancel=True)
+ employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
if self.new_company != self.company:
employee.company = self.company
employee.save()
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index ad2f3ade054..c0440d09e74 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import unittest
+from datetime import date
import frappe
from frappe.utils import add_days, getdate
@@ -15,7 +16,12 @@ class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
make_employee("employee2@transfers.com")
make_employee("employee3@transfers.com")
- frappe.db.sql("""delete from `tabEmployee Transfer`""")
+ create_company()
+ create_employee()
+ create_employee_transfer()
+
+ def tearDown(self):
+ frappe.db.rollback()
def test_submit_before_transfer_date(self):
transfer_obj = frappe.get_doc({
@@ -57,3 +63,77 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertTrue(transfer.new_employee_id)
self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active")
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
+
+ def test_employee_history(self):
+ name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
+ doc = frappe.get_doc("Employee",name)
+ count = 0
+ department = ["Accounts - TC", "Management - TC"]
+ designation = ["Accountant", "Manager"]
+ dt = [getdate("01-10-2021"), date.today()]
+
+ for data in doc.internal_work_history:
+ self.assertEqual(data.department, department[count])
+ self.assertEqual(data.designation, designation[count])
+ self.assertEqual(data.from_date, dt[count])
+ count = count + 1
+
+ data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
+ doc = frappe.get_doc("Employee Transfer", data[0]["name"])
+ doc.cancel()
+ employee_doc = frappe.get_doc("Employee",name)
+
+ for data in employee_doc.internal_work_history:
+ self.assertEqual(data.designation, designation[0])
+ self.assertEqual(data.department, department[0])
+ self.assertEqual(data.from_date, dt[0])
+
+def create_employee():
+ doc = frappe.get_doc({
+ "doctype": "Employee",
+ "first_name": "John",
+ "company": "Test Company",
+ "gender": "Male",
+ "date_of_birth": getdate("30-09-1980"),
+ "date_of_joining": getdate("01-10-2021"),
+ "department": "Accounts - TC",
+ "designation": "Accountant"
+ })
+
+ doc.save()
+
+def create_company():
+ exists = frappe.db.exists("Company", "Test Company")
+ if not exists:
+ doc = frappe.get_doc({
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India"
+ })
+
+ doc.save()
+
+def create_employee_transfer():
+ doc = frappe.get_doc({
+ "doctype": "Employee Transfer",
+ "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
+ "transfer_date": date.today(),
+ "transfer_details": [
+ {
+ "property": "Designation",
+ "current": "Accountant",
+ "new": "Manager",
+ "fieldname": "designation"
+ },
+ {
+ "property": "Department",
+ "current": "Accounts - TC",
+ "new": "Management - TC",
+ "fieldname": "department"
+ }
+ ]
+ })
+
+ doc.save()
+ doc.submit()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
index 4a1064b66b7..2f7b8fcf679 100644
--- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
+++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
@@ -100,7 +100,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-09-23 20:27:36.027728",
+ "modified": "2021-10-26 20:27:36.027728",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Taxes and Charges",
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 8df5cb582e3..0b2f99c358e 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -145,7 +145,15 @@ def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
-def update_employee(employee, details, date=None, cancel=False):
+def update_employee_work_history(employee, details, date=None, cancel=False):
+ if not employee.internal_work_history and not cancel:
+ employee.append("internal_work_history", {
+ "branch": employee.branch,
+ "designation": employee.designation,
+ "department": employee.department,
+ "from_date": employee.date_of_joining
+ })
+
internal_work_history = {}
for item in details:
field = frappe.get_meta("Employee").get_field(item.fieldname)
@@ -160,11 +168,35 @@ def update_employee(employee, details, date=None, cancel=False):
setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new
+
if internal_work_history and not cancel:
internal_work_history["from_date"] = date
employee.append("internal_work_history", internal_work_history)
+
+ if cancel:
+ delete_employee_work_history(details, employee, date)
+
return employee
+def delete_employee_work_history(details, employee, date):
+ filters = {}
+ for d in details:
+ for history in employee.internal_work_history:
+ if d.property == "Department" and history.department == d.new:
+ department = d.new
+ filters["department"] = department
+ if d.property == "Designation" and history.designation == d.new:
+ designation = d.new
+ filters["designation"] = designation
+ if d.property == "Branch" and history.branch == d.new:
+ branch = d.new
+ filters["branch"] = branch
+ if date and date == history.from_date:
+ filters["from_date"] = date
+ if filters:
+ frappe.db.delete("Employee Internal Work History", filters)
+
+
@frappe.whitelist()
def get_employee_fields_label():
fields = []
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 7e539183b0c..62187077f3d 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -436,7 +436,7 @@
"description": "Item Image (if not slideshow)",
"fieldname": "website_image",
"fieldtype": "Attach Image",
- "label": "Image"
+ "label": "Website Image"
},
{
"allow_on_submit": 1,
@@ -539,7 +539,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-05-16 12:25:09.081968",
+ "modified": "2021-10-27 14:52:04.500251",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 3ea756eec97..0ac64c2cfca 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -308,6 +308,9 @@ class BOM(WebsiteGenerator):
existing_bom_cost = self.total_cost
for d in self.get("items"):
+ if not d.item_code:
+ continue
+
rate = self.get_rm_rate({
"company": self.company,
"item_code": d.item_code,
@@ -600,7 +603,7 @@ class BOM(WebsiteGenerator):
for d in self.get('items'):
if d.bom_no:
self.get_child_exploded_items(d.bom_no, d.stock_qty)
- else:
+ elif d.item_code:
self.add_to_cur_exploded_items(frappe._dict({
'item_code' : d.item_code,
'item_name' : d.item_name,
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index b9efe9b41ea..2424ef9a71c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -311,7 +311,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0:
self.status = "In Process"
- if self.total_produced_qty >= self.total_planned_qty:
+ if self.check_have_work_orders_completed():
self.status = "Completed"
if self.status != 'Completed':
@@ -424,7 +424,7 @@ class ProductionPlan(Document):
po = frappe.new_doc('Purchase Order')
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
- po.is_subcontracted_item = 'Yes'
+ po.is_subcontracted = 'Yes'
for row in po_list:
args = {
'item_code': row.production_item,
@@ -575,6 +575,15 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data)
+ def check_have_work_orders_completed(self):
+ wo_status = frappe.db.get_list(
+ "Work Order",
+ filters={"production_plan": self.name},
+ fields="status",
+ pluck="status"
+ )
+ return all(s == "Completed" for s in wo_status)
+
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
if isinstance(doc, str):
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 913fc85af61..7f8e816a22a 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -182,6 +182,7 @@
"reqd": 1
},
{
+ "default": "1.0",
"fieldname": "qty",
"fieldtype": "Float",
"label": "Qty To Manufacture",
@@ -572,10 +573,11 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-24 15:14:03.844937",
+ "modified": "2021-10-27 19:21:35.139888",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
+ "naming_rule": "By \"Naming Series\" field",
"nsm_parent_field": "parent_work_order",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e282dd3ecba..f881e1bf16a 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -685,9 +685,7 @@ class WorkOrder(Document):
if not d.operation:
d.operation = operation
else:
- # Attribute a big number (999) to idx for sorting putpose in case idx is NULL
- # For instance in BOM Explosion Item child table, the items coming from sub assembly items
- for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
+ for item in sorted(item_dict.values(), key=lambda d: d['idx'] or float('inf')):
self.append('required_items', {
'rate': item.rate,
'amount': item.rate * item.qty,
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
index a7aec315ff2..74bd685b799 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
@@ -24,7 +24,7 @@ def get_data(filters):
}
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
- "total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"]
+ "total_completed_qty", "workstation", "operation", "total_time_in_mins"]
for field in ["work_order", "workstation", "operation", "company"]:
if filters.get(field):
@@ -45,7 +45,7 @@ def get_data(filters):
job_card_time_details = {}
for job_card_data in frappe.get_all("Job Card Time Log",
fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"],
- filters=job_card_time_filter, group_by="parent", debug=1):
+ filters=job_card_time_filter, group_by="parent"):
job_card_time_details[job_card_data.parent] = job_card_data
res = []
@@ -172,12 +172,6 @@ def get_columns(filters):
"options": "Operation",
"width": 110
},
- {
- "label": _("Employee Name"),
- "fieldname": "employee_name",
- "fieldtype": "Data",
- "width": 110
- },
{
"label": _("Total Completed Qty"),
"fieldname": "total_completed_qty",
diff --git a/erpnext/manufacturing/report/process_loss_report/__init__.py b/erpnext/manufacturing/report/process_loss_report/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js
similarity index 100%
rename from erpnext/stock/report/process_loss_report/process_loss_report.js
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.js
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/manufacturing/report/process_loss_report/process_loss_report.json
similarity index 83%
rename from erpnext/stock/report/process_loss_report/process_loss_report.json
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.json
index afe4aff7f1c..7d3d13d98cf 100644
--- a/erpnext/stock/report/process_loss_report/process_loss_report.json
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.json
@@ -9,9 +9,9 @@
"filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2021-08-24 16:38:15.233395",
+ "modified": "2021-10-20 22:03:57.606612",
"modified_by": "Administrator",
- "module": "Stock",
+ "module": "Manufacturing",
"name": "Process Loss Report",
"owner": "Administrator",
"prepared_report": 0,
@@ -21,9 +21,6 @@
"roles": [
{
"role": "Manufacturing User"
- },
- {
- "role": "Stock User"
}
]
}
\ No newline at end of file
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
similarity index 98%
rename from erpnext/stock/report/process_loss_report/process_loss_report.py
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.py
index 5c6a3bb4566..d3dfd52b773 100644
--- a/erpnext/stock/report/process_loss_report/process_loss_report.py
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
@@ -111,7 +111,7 @@ def run_query(query_args: QueryArgs) -> Data:
{work_order_filter}
GROUP BY
se.work_order
- """.format(**query_args), query_args, as_dict=1, debug=1)
+ """.format(**query_args), query_args, as_dict=1)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
new file mode 100644
index 00000000000..1de472659eb
--- /dev/null
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -0,0 +1,64 @@
+import unittest
+from typing import List, Tuple
+
+import frappe
+
+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",
+ "warehouse": "_Test Warehouse - _TC",
+}
+
+
+REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
+ ("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
+ ("BOM Operations Time", {}),
+ ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
+ ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
+ ("Cost of Poor Quality Report", {}),
+ ("Downtime Analysis", {}),
+ (
+ "Exponential Smoothing Forecasting",
+ {
+ "based_on_document": "Sales Order",
+ "based_on_field": "Qty",
+ "no_of_years": 3,
+ "periodicity": "Yearly",
+ "smoothing_constant": 0.3,
+ },
+ ),
+ ("Job Card Summary", {"fiscal_year": "2021-2022"}),
+ ("Production Analytics", {"range": "Monthly"}),
+ ("Quality Inspection Summary", {}),
+ ("Process Loss Report", {}),
+ ("Work Order Stock Report", {}),
+ ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
+]
+
+
+if frappe.db.a_row_exists("Production Plan"):
+ REPORT_FILTER_TEST_CASES.append(
+ ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
+ )
+
+OPTIONAL_FILTERS = {
+ "warehouse": "_Test Warehouse - _TC",
+ "item": "_Test Item",
+ "item_group": "_Test Item Group",
+}
+
+
+class TestManufacturingReports(unittest.TestCase):
+ def test_execute_all_manufacturing_reports(self):
+ """Test that all script report in manufacturing modules are executable with supported filters"""
+ for report, filter in REPORT_FILTER_TEST_CASES:
+ execute_script_report(
+ report_name=report,
+ module="Manufacturing",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index fbe33dace83..2ad2fea6847 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -306,6 +306,8 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
@@ -327,3 +329,5 @@ erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
+erpnext.patches.v12_0.update_production_plan_status
+erpnext.patches.v13_0.update_category_in_ltds_certificate
diff --git a/erpnext/patches/v12_0/update_production_plan_status.py b/erpnext/patches/v12_0/update_production_plan_status.py
new file mode 100644
index 00000000000..06fc503a33f
--- /dev/null
+++ b/erpnext/patches/v12_0/update_production_plan_status.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2021, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "production_plan")
+ frappe.db.sql("""
+ UPDATE `tabProduction Plan` ppl
+ SET status = "Completed"
+ WHERE ppl.name IN (
+ SELECT ss.name FROM (
+ SELECT
+ (
+ count(wo.status = "Completed") =
+ count(pp.name)
+ ) =
+ (
+ pp.status != "Completed"
+ AND pp.total_produced_qty >= pp.total_planned_qty
+ ) AS should_set,
+ pp.name AS name
+ FROM
+ `tabWork Order` wo INNER JOIN`tabProduction Plan` pp
+ ON wo.production_plan = pp.name
+ GROUP BY pp.name
+ HAVING should_set = 1
+ ) ss
+ )
+ """)
diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
index 43a9aeb6fe6..e136d64bb56 100644
--- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
+++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from erpnext.regional.united_states.setup import add_permissions
+from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import add_permissions
def execute():
@@ -11,7 +11,12 @@ def execute():
if not company:
return
- frappe.reload_doc("regional", "doctype", "product_tax_category")
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+ TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+ if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE):
+ return
custom_fields = {
'Sales Invoice Item': [
@@ -29,4 +34,4 @@ def execute():
}
create_custom_fields(custom_fields, update=True)
add_permissions()
- frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True)
\ No newline at end of file
+ frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True)
diff --git a/erpnext/patches/v13_0/update_category_in_ltds_certificate.py b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py
new file mode 100644
index 00000000000..4d4645269cb
--- /dev/null
+++ b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py
@@ -0,0 +1,18 @@
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc")
+ supplier = frappe.qb.DocType("Supplier")
+
+ frappe.qb.update(ldc).inner_join(supplier).on(
+ ldc.supplier == supplier.name
+ ).set(
+ ldc.tax_withholding_category, supplier.tax_withholding_category
+ ).where(
+ ldc.tax_withholding_category.isnull()
+ ).run()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index 7c0a8eac99c..b6377f40066 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -125,27 +125,28 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
+@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
- additional_salary_list = frappe.db.sql("""
- select name, salary_component as component, type, amount,
- overwrite_salary_structure_amount as overwrite,
- deduct_full_tax_on_selected_payroll_date
- from `tabAdditional Salary`
- where employee=%(employee)s
- and docstatus = 1
- and (
- payroll_date between %(from_date)s and %(to_date)s
- or
- from_date <= %(to_date)s and to_date >= %(to_date)s
- )
- and type = %(component_type)s
- order by salary_component, overwrite ASC
- """, {
- 'employee': employee,
- 'from_date': start_date,
- 'to_date': end_date,
- 'component_type': "Earning" if component_type == "earnings" else "Deduction"
- }, as_dict=1)
+ comp_type = 'Earning' if component_type == 'earnings' else 'Deduction'
+
+ additional_sal = frappe.qb.DocType('Additional Salary')
+ component_field = additional_sal.salary_component.as_('component')
+ overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite')
+
+ additional_salary_list = frappe.qb.from_(
+ additional_sal
+ ).select(
+ additional_sal.name, component_field, additional_sal.type,
+ additional_sal.amount, additional_sal.is_recurring, overwrite_field,
+ additional_sal.deduct_full_tax_on_selected_payroll_date
+ ).where(
+ (additional_sal.employee == employee)
+ & (additional_sal.docstatus == 1)
+ & (additional_sal.type == comp_type)
+ ).where(
+ additional_sal.payroll_date[start_date: end_date]
+ | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date))
+ ).run(as_dict=True)
additional_salaries = []
components_to_overwrite = []
diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json
index 393f647cc88..665f0a8297e 100644
--- a/erpnext/payroll/doctype/salary_detail/salary_detail.json
+++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json
@@ -12,6 +12,7 @@
"year_to_date",
"section_break_5",
"additional_salary",
+ "is_recurring_additional_salary",
"statistical_component",
"depends_on_payment_days",
"exempted_from_income_tax",
@@ -235,11 +236,19 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
- }
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.additional_salary",
+ "fieldname": "is_recurring_additional_salary",
+ "fieldtype": "Check",
+ "label": "Is Recurring Additional Salary",
+ "read_only": 1
+ }
],
"istable": 1,
"links": [],
- "modified": "2021-01-14 13:39:15.847158",
+ "modified": "2021-08-30 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 888150f0ae3..28e6a3ccbba 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -172,7 +172,6 @@ class SalarySlip(TransactionBase):
and employee = %s and name != %s {0}""".format(cond),
(self.start_date, self.end_date, self.employee, self.name))
if ret_exist:
- self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
else:
for data in self.timesheets:
@@ -630,7 +629,8 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component),
additional_salary.amount,
component_type,
- additional_salary
+ additional_salary,
+ is_recurring = additional_salary.is_recurring
)
def add_tax_components(self, payroll_period):
@@ -651,7 +651,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
- def update_component_row(self, component_data, amount, component_type, additional_salary=None):
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
@@ -702,6 +702,7 @@ class SalarySlip(TransactionBase):
component_row.default_amount = 0
component_row.additional_amount = amount
+ component_row.is_recurring_additional_salary = is_recurring
component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date
@@ -898,25 +899,33 @@ class SalarySlip(TransactionBase):
amount, additional_amount = earning.default_amount, earning.additional_amount
if earning.is_tax_applicable:
- if additional_amount:
- taxable_earnings += (amount - additional_amount)
- additional_income += additional_amount
- if earning.deduct_full_tax_on_selected_payroll_date:
- additional_income_with_full_tax += additional_amount
- continue
-
if earning.is_flexible_benefit:
flexi_benefits += amount
else:
- taxable_earnings += amount
+ taxable_earnings += (amount - additional_amount)
+ additional_income += additional_amount
+
+ # Get additional amount based on future recurring additional salary
+ if additional_amount and earning.is_recurring_additional_salary:
+ additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
+ earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
+
+ if earning.deduct_full_tax_on_selected_payroll_date:
+ additional_income_with_full_tax += additional_amount
if allow_tax_exemption:
for ded in self.deductions:
if ded.exempted_from_income_tax:
- amount = ded.amount
+ amount, additional_amount = ded.amount, ded.additional_amount
if based_on_payment_days:
- amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0]
- taxable_earnings -= flt(amount)
+ amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)
+
+ taxable_earnings -= flt(amount - additional_amount)
+ additional_income -= additional_amount
+
+ if additional_amount and ded.is_recurring_additional_salary:
+ additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
+ ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
return frappe._dict({
"taxable_earnings": taxable_earnings,
@@ -925,11 +934,21 @@ class SalarySlip(TransactionBase):
"flexi_benefits": flexi_benefits
})
+ def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
+ future_recurring_additional_amount = 0
+ to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
+ # future month count excluding current
+ future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
+ if future_recurring_period > 0:
+ future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
+ return future_recurring_additional_amount
+
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
if (self.salary_structure and
- cint(row.depends_on_payment_days) and cint(self.total_working_days) and
- (not self.salary_slip_based_on_timesheet or
+ cint(row.depends_on_payment_days) and cint(self.total_working_days)
+ and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
+ and (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
@@ -1248,7 +1267,7 @@ class SalarySlip(TransactionBase):
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'],
- filters = {'employee_name' : self.employee_name,
+ filters = {'employee' : self.employee,
'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date],
'name': ['!=', self.name],
@@ -1268,7 +1287,7 @@ class SalarySlip(TransactionBase):
first_day_of_the_month = get_first_day(self.start_date)
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
- filters = {'employee_name' : self.employee_name,
+ filters = {'employee' : self.employee,
'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date],
'name': ['!=', self.name],
@@ -1292,13 +1311,13 @@ class SalarySlip(TransactionBase):
INNER JOIN `tabSalary Slip` as salary_slip
ON detail.parent = salary_slip.name
WHERE
- salary_slip.employee_name = %(employee_name)s
+ salary_slip.employee = %(employee)s
AND detail.salary_component = %(component)s
AND salary_slip.start_date >= %(period_start_date)s
AND salary_slip.end_date < %(period_end_date)s
AND salary_slip.name != %(docname)s
AND salary_slip.docstatus = 1""",
- {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
+ {'employee': self.employee, 'component': component.salary_component, 'period_start_date': period_start_date,
'period_end_date': period_end_date, 'docname': self.name}
)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 91b109bb408..a44af09b793 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -540,6 +540,61 @@ class TestSalarySlip(unittest.TestCase):
# undelete fixture data
frappe.db.rollback()
+ def test_tax_for_recurring_additional_salary(self):
+ frappe.db.sql("""delete from `tabPayroll Period`""")
+ frappe.db.sql("""delete from `tabSalary Component`""")
+
+ payroll_period = create_payroll_period()
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True)
+
+ employee = make_employee("test_tax@salary.slip")
+ delete_docs = [
+ "Salary Slip",
+ "Additional Salary",
+ "Employee Tax Exemption Declaration",
+ "Employee Tax Exemption Proof Submission",
+ "Employee Benefit Claim",
+ "Salary Structure Assignment"
+ ]
+ for doc in delete_docs:
+ frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee))
+
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+ salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
+ other_details={"max_benefits": 100000}, test_tax=True,
+ employee=employee, payroll_period=payroll_period)
+
+
+ create_salary_slips_for_payroll_period(employee, salary_structure.name,
+ payroll_period, deduct_random=False, num=3)
+
+ tax_paid = get_tax_paid_in_period(employee)
+
+ annual_tax = 23196.0
+ self.assertEqual(tax_paid, annual_tax)
+
+ frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
+
+ #------------------------------------
+ # Recurring additional salary
+ start_date = add_months(payroll_period.start_date, 3)
+ end_date = add_months(payroll_period.start_date, 5)
+ create_recurring_additional_salary(employee, "Performance Bonus", 20000, start_date, end_date)
+
+ frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
+
+ create_salary_slips_for_payroll_period(employee, salary_structure.name,
+ payroll_period, deduct_random=False, num=4)
+
+ tax_paid = get_tax_paid_in_period(employee)
+
+ annual_tax = 32315.0
+ self.assertEqual(tax_paid, annual_tax)
+
+ frappe.db.rollback()
+
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
@@ -1010,4 +1065,18 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure
else:
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
- return salary_slip
\ No newline at end of file
+ return salary_slip
+
+def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None):
+ frappe.get_doc({
+ "doctype": "Additional Salary",
+ "employee": employee,
+ "company": company or erpnext.get_default_company(),
+ "salary_component": salary_component,
+ "is_recurring": 1,
+ "from_date": from_date,
+ "to_date": to_date,
+ "amount": amount,
+ "type": "Earning",
+ "currency": erpnext.get_default_currency()
+ }).submit()
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 1655b76b988..f615f051f0c 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -32,12 +32,12 @@ frappe.ui.form.on("Timesheet", {
};
},
- onload: function(frm){
+ onload: function(frm) {
if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm);
}
- if (frm.is_new()) {
+ if (frm.is_new() && !frm.doc.employee) {
set_employee_and_company(frm);
}
},
@@ -283,7 +283,9 @@ frappe.ui.form.on("Timesheet Detail", {
calculate_time_and_amount(frm);
},
- activity_type: function(frm, cdt, cdn) {
+ activity_type: function (frm, cdt, cdn) {
+ if (!frappe.get_doc(cdt, cdn).activity_type) return;
+
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
@@ -291,10 +293,10 @@ frappe.ui.form.on("Timesheet Detail", {
activity_type: frm.selected_doc.activity_type,
currency: frm.doc.currency
},
- callback: function(r){
- if(r.message){
- frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']);
- frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']);
+ callback: function (r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "billing_rate", r.message["billing_rate"]);
+ frappe.model.set_value(cdt, cdn, "costing_rate", r.message["costing_rate"]);
calculate_billing_costing_amount(frm, cdt, cdn);
}
}
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 90cb5559394..019f3fbec12 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -137,7 +137,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
var me = this;
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
- tax.item_wise_tax_detail = {};
+ if (!tax.dont_recompute_tax) {
+ tax.item_wise_tax_detail = {};
+ }
var tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"];
@@ -419,7 +421,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
current_tax_amount = tax_rate * item.qty;
}
- this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
+ if (!tax.dont_recompute_tax) {
+ this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
+ }
return current_tax_amount;
},
@@ -587,7 +591,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
delete tax[fieldname];
});
- tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
+ if (!tax.dont_recompute_tax) {
+ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
+ }
});
}
},
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js
index 0d79b10c041..1a309ba0156 100644
--- a/erpnext/public/js/financial_statements.js
+++ b/erpnext/public/js/financial_statements.js
@@ -113,15 +113,15 @@ function get_filters() {
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
- "hidden": 1,
- "reqd": 1
+ "reqd": 1,
+ "depends_on": "eval:doc.filter_based_on == 'Date Range'"
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
- "hidden": 1,
- "reqd": 1
+ "reqd": 1,
+ "depends_on": "eval:doc.filter_based_on == 'Date Range'"
},
{
"fieldname":"from_fiscal_year",
@@ -129,7 +129,8 @@ function get_filters() {
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
- "reqd": 1
+ "reqd": 1,
+ "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
},
{
"fieldname":"to_fiscal_year",
@@ -137,7 +138,8 @@ function get_filters() {
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
- "reqd": 1
+ "reqd": 1,
+ "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
},
{
"fieldname": "periodicity",
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index e07bcbd28ec..429f4ca35df 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -459,6 +459,7 @@ body.product-page {
min-height: 0px;
.r-item-image {
+ min-height: 100px;
width: 40%;
.r-product-image {
@@ -480,6 +481,7 @@ body.product-page {
.r-item-info {
font-size: 14px;
padding-right: 0;
+ padding-left: 10px;
width: 60%;
a {
@@ -672,18 +674,6 @@ body.product-page {
img {
max-height: 112px;
}
-
- .no-image-cart-item {
- max-height: 112px;
- display: flex; justify-content: center;
- background-color: var(--gray-200);
- align-items: center;
- color: var(--gray-400);
- margin-top: .15rem;
- border-radius: 6px;
- height: 100%;
- font-size: 24px;
- }
}
.cart-items {
@@ -862,6 +852,18 @@ body.product-page {
}
}
+.no-image-cart-item {
+ max-height: 112px;
+ display: flex; justify-content: center;
+ background-color: var(--gray-200);
+ align-items: center;
+ color: var(--gray-400);
+ margin-top: .15rem;
+ border-radius: 6px;
+ height: 100%;
+ font-size: 24px;
+}
+
.cart-empty.frappe-card {
min-height: 76vh;
@include flex(flex, center, center, column);
diff --git a/erpnext/regional/__init__.py b/erpnext/regional/__init__.py
index 45a689efa8b..d7dcbf4fe18 100644
--- a/erpnext/regional/__init__.py
+++ b/erpnext/regional/__init__.py
@@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
"document_name": doc.name,
"data": data
}).insert(ignore_permissions=True)
+
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py b/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
new file mode 100644
index 00000000000..89ba3e977af
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2021-07-13 09:17:09.862163",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "item_tax_template",
+ "account"
+ ],
+ "fields": [
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Tax Template",
+ "options": "Item Tax Template",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-08-04 06:42:38.205597",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Purchase Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
new file mode 100644
index 00000000000..3920bc546c1
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATPurchaseAccount(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py b/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
new file mode 100644
index 00000000000..72613f4064f
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Havenir Solutions and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('KSA VAT Sales Account', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
new file mode 100644
index 00000000000..df2747891dc
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2021-07-13 08:46:33.820968",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "item_tax_template",
+ "account"
+ ],
+ "fields": [
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Tax Template",
+ "options": "Item Tax Template",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-08-04 06:42:00.081407",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Sales Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
new file mode 100644
index 00000000000..7c2689f530e
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATSalesAccount(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
new file mode 100644
index 00000000000..1d6a6a793dc
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestKSAVATSalesAccount(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/__init__.py b/erpnext/regional/doctype/ksa_vat_setting/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
new file mode 100644
index 00000000000..00b62b9adfb
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Havenir Solutions and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('KSA VAT Setting', {
+ onload: function () {
+ frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
+ }
+});
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
new file mode 100644
index 00000000000..33619467ed0
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "autoname": "field:company",
+ "creation": "2021-07-13 08:49:01.100356",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "ksa_vat_sales_accounts",
+ "ksa_vat_purchase_accounts"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "ksa_vat_sales_accounts",
+ "fieldtype": "Table",
+ "label": "KSA VAT Sales Accounts",
+ "options": "KSA VAT Sales Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ksa_vat_purchase_accounts",
+ "fieldtype": "Table",
+ "label": "KSA VAT Purchase Accounts",
+ "options": "KSA VAT Purchase Account",
+ "reqd": 1
+ }
+ ],
+ "links": [],
+ "modified": "2021-08-26 04:29:06.499378",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Setting",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "company",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
new file mode 100644
index 00000000000..bdae1161fd7
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATSetting(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
new file mode 100644
index 00000000000..269cbec5fb4
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
@@ -0,0 +1,5 @@
+frappe.listview_settings['KSA VAT Setting'] = {
+ onload () {
+ frappe.breadcrumbs.add('Accounts');
+ }
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
new file mode 100644
index 00000000000..7207901fd43
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestKSAVATSetting(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
index f48fe6f4763..c32ab6bec24 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
@@ -7,7 +7,7 @@
"engine": "InnoDB",
"field_order": [
"certificate_details_section",
- "section_code",
+ "tax_withholding_category",
"fiscal_year",
"column_break_3",
"certificate_no",
@@ -33,13 +33,6 @@
"reqd": 1,
"unique": 1
},
- {
- "fieldname": "section_code",
- "fieldtype": "Select",
- "label": "Section Code",
- "options": "192\n193\n194\n194A\n194C\n194D\n194H\n194I\n194J\n194LA\n194LBB\n194LBC\n195",
- "reqd": 1
- },
{
"fieldname": "section_break_3",
"fieldtype": "Section Break",
@@ -123,13 +116,22 @@
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
+ },
+ {
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category",
+ "reqd": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-04-23 23:04:41.203721",
+ "modified": "2021-10-23 18:33:38.962622",
"modified_by": "Administrator",
"module": "Regional",
"name": "Lower Deduction Certificate",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
index d8553f1d913..7afbc00980c 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
@@ -15,7 +15,7 @@ from erpnext.accounts.utils import get_fiscal_year
class LowerDeductionCertificate(Document):
def validate(self):
self.validate_dates()
- self.validate_supplier_against_section_code()
+ self.validate_supplier_against_tax_category()
def validate_dates(self):
if getdate(self.valid_upto) < getdate(self.valid_from):
@@ -31,12 +31,14 @@ class LowerDeductionCertificate(Document):
<= fiscal_year.year_end_date):
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
- def validate_supplier_against_section_code(self):
- duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name', 'valid_from', 'valid_upto'], as_dict=True)
+ def tax_withholding_category(self):
+ duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate',
+ {'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)},
+ ['name', 'valid_from', 'valid_upto'], as_dict=True)
if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate):
certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name)
- frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against Section Code {2} for this time period.")
- .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.section_code)))
+ frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period.")
+ .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category)))
def are_dates_overlapping(self,duplicate_certificate):
valid_from = duplicate_certificate.valid_from
diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json
index 60f490d6166..c2a28f20494 100644
--- a/erpnext/regional/india/e_invoice/einv_template.json
+++ b/erpnext/regional/india/e_invoice/einv_template.json
@@ -38,7 +38,7 @@
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
- "Nm": "{dispatch_details.company_name}",
+ "Nm": "{dispatch_details.legal_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index f7aa65776b0..cd2c8a26b1c 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -138,8 +138,8 @@ def get_doc_details(invoice):
invoice_date=invoice_date
))
-def validate_address_fields(address, is_shipping_address):
- if ((not address.gstin and not is_shipping_address)
+def validate_address_fields(address, skip_gstin_validation):
+ if ((not address.gstin and not skip_gstin_validation)
or not address.city
or not address.pincode
or not address.address_title
@@ -151,10 +151,10 @@ def validate_address_fields(address, is_shipping_address):
title=_('Missing Address Fields')
)
-def get_party_details(address_name, is_shipping_address=False):
+def get_party_details(address_name, skip_gstin_validation=False):
addr = frappe.get_doc('Address', address_name)
- validate_address_fields(addr, is_shipping_address)
+ validate_address_fields(addr, skip_gstin_validation)
if addr.gst_state_number == 97:
# according to einvoice standard
@@ -443,7 +443,11 @@ def make_einvoice(invoice):
if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
- shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True)
+ shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True)
+
+ dispatch_details = frappe._dict({})
+ if invoice.dispatch_address_name:
+ dispatch_details = get_party_details(invoice.dispatch_address_name, skip_gstin_validation=True)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
@@ -455,7 +459,7 @@ def make_einvoice(invoice):
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
- dispatch_details = period_details = export_details = frappe._dict({})
+ period_details = export_details = frappe._dict({})
einvoice = schema.format(
transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
diff --git a/erpnext/regional/report/ksa_vat/__init__.py b/erpnext/regional/report/ksa_vat/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js
new file mode 100644
index 00000000000..d46d260ac1e
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.js
@@ -0,0 +1,60 @@
+// Copyright (c) 2016, Havenir Solutions and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["KSA VAT"] = {
+ onload() {
+ frappe.breadcrumbs.add('Accounts');
+ },
+ "filters": [
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.get_today()
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ if (data
+ && (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
+ && data.title==value) {
+ value = $(`
${value} `);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("
").parent().html();
+ return value
+ }else if (data.title=='Grand Total'){
+ if (data.title==value) {
+ value = $(`
${value} `);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("
").parent().html();
+ return value
+ }else{
+ value = default_formatter(value, row, column, data);
+ value = $(`
${value} `);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("
").parent().html();
+ console.log($value)
+ return value
+ }
+ }else{
+ value = default_formatter(value, row, column, data);
+ return value;
+ }
+ },
+};
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.json b/erpnext/regional/report/ksa_vat/ksa_vat.json
new file mode 100644
index 00000000000..036e2603103
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-07-13 08:54:38.000949",
+ "disable_prepared_report": 1,
+ "disabled": 1,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-08-26 04:14:37.202594",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT",
+ "owner": "Administrator",
+ "prepared_report": 1,
+ "ref_doctype": "GL Entry",
+ "report_name": "KSA VAT",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Accounts User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py
new file mode 100644
index 00000000000..a42ebc9f7e5
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.py
@@ -0,0 +1,176 @@
+# Copyright (c) 2013, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import get_url_to_list
+
+
+def execute(filters=None):
+ columns = columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_columns():
+ return [
+ {
+ "fieldname": "title",
+ "label": _("Title"),
+ "fieldtype": "Data",
+ "width": 300
+ },
+ {
+ "fieldname": "amount",
+ "label": _("Amount (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ },
+ {
+ "fieldname": "adjustment_amount",
+ "label": _("Adjustment (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ },
+ {
+ "fieldname": "vat_amount",
+ "label": _("VAT Amount (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ }
+ ]
+
+def get_data(filters):
+ data = []
+
+ # Validate if vat settings exist
+ company = filters.get('company')
+ if frappe.db.exists('KSA VAT Setting', company) is None:
+ url = get_url_to_list('KSA VAT Setting')
+ frappe.msgprint(_('Create
KSA VAT Setting for this company').format(url))
+ return data
+
+ ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
+
+ # Sales Heading
+ append_data(data, 'VAT on Sales', '', '', '')
+
+ grand_total_taxable_amount = 0
+ grand_total_taxable_adjustment_amount = 0
+ grand_total_tax = 0
+
+ for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
+ total_taxable_amount, total_taxable_adjustment_amount, \
+ total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice')
+
+ # Adding results to data
+ append_data(data, vat_setting.title, total_taxable_amount,
+ total_taxable_adjustment_amount, total_tax)
+
+ grand_total_taxable_amount += total_taxable_amount
+ grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
+ grand_total_tax += total_tax
+
+ # Sales Grand Total
+ append_data(data, 'Grand Total', grand_total_taxable_amount,
+ grand_total_taxable_adjustment_amount, grand_total_tax)
+
+ # Blank Line
+ append_data(data, '', '', '', '')
+
+ # Purchase Heading
+ append_data(data, 'VAT on Purchases', '', '', '')
+
+ grand_total_taxable_amount = 0
+ grand_total_taxable_adjustment_amount = 0
+ grand_total_tax = 0
+
+ for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
+ total_taxable_amount, total_taxable_adjustment_amount, \
+ total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice')
+
+ # Adding results to data
+ append_data(data, vat_setting.title, total_taxable_amount,
+ total_taxable_adjustment_amount, total_tax)
+
+ grand_total_taxable_amount += total_taxable_amount
+ grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
+ grand_total_tax += total_tax
+
+ # Purchase Grand Total
+ append_data(data, 'Grand Total', grand_total_taxable_amount,
+ grand_total_taxable_adjustment_amount, grand_total_tax)
+
+ return data
+
+def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
+ '''
+ (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
+ calculates and returns \n
+ total_taxable_amount, total_taxable_adjustment_amount, total_tax'''
+ from_date = filters.get('from_date')
+ to_date = filters.get('to_date')
+
+ # Initiate variables
+ total_taxable_amount = 0
+ total_taxable_adjustment_amount = 0
+ total_tax = 0
+ # Fetch All Invoices
+ invoices = frappe.get_list(doctype,
+ filters ={
+ 'docstatus': 1,
+ 'posting_date': ['between', [from_date, to_date]]
+ }, fields =['name', 'is_return'])
+
+ for invoice in invoices:
+ invoice_items = frappe.get_list(f'{doctype} Item',
+ filters ={
+ 'docstatus': 1,
+ 'parent': invoice.name,
+ 'item_tax_template': vat_setting.item_tax_template
+ }, fields =['item_code', 'net_amount'])
+
+ for item in invoice_items:
+ # Summing up total taxable amount
+ if invoice.is_return == 0:
+ total_taxable_amount += item.net_amount
+
+ if invoice.is_return == 1:
+ total_taxable_adjustment_amount += item.net_amount
+
+ # Summing up total tax
+ total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
+
+ return total_taxable_amount, total_taxable_adjustment_amount, total_tax
+
+
+
+def append_data(data, title, amount, adjustment_amount, vat_amount):
+ """Returns data with appended value."""
+ data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
+
+def get_tax_amount(item_code, account_head, doctype, parent):
+ if doctype == 'Sales Invoice':
+ tax_doctype = 'Sales Taxes and Charges'
+
+ elif doctype == 'Purchase Invoice':
+ tax_doctype = 'Purchase Taxes and Charges'
+
+ item_wise_tax_detail = frappe.get_value(tax_doctype, {
+ 'docstatus': 1,
+ 'parent': parent,
+ 'account_head': account_head
+ }, 'item_wise_tax_detail')
+
+ tax_amount = 0
+ if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
+ item_wise_tax_detail = json.loads(item_wise_tax_detail)
+ for key, value in item_wise_tax_detail.items():
+ if key == item_code:
+ tax_amount = value[1]
+ break
+
+ return tax_amount
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
index f4c049d1623..2b5ecc3b18c 100644
--- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
@@ -122,7 +122,7 @@ def get_total_emiratewise(filters):
try:
return frappe.db.sql("""
select
- s.vat_emirate as emirate, sum(i.base_amount) as total, sum(s.total_taxes_and_charges)
+ s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount)
from
`tabSales Invoice Item` i inner join `tabSales Invoice` s
on
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 3ccaae9e6a5..6113f48d3f1 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -2,10 +2,36 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-
-from erpnext.regional.united_arab_emirates.setup import add_print_formats, make_custom_fields
-
+import frappe
+from frappe.permissions import add_permission, update_permission_property
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
+from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def setup(company=None, patch=True):
- make_custom_fields()
+ uae_custom_fields()
add_print_formats()
+ add_permissions()
+ create_ksa_vat_setting(company)
+ make_qrcode_field()
+
+def add_permissions():
+ """Add Permissions for KSA VAT Setting."""
+ add_permission('KSA VAT Setting', 'All', 0)
+ for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
+ add_permission('KSA VAT Setting', role, 0)
+ update_permission_property('KSA VAT Setting', role, 0, 'write', 1)
+ update_permission_property('KSA VAT Setting', role, 0, 'create', 1)
+
+ """Enable KSA VAT Report"""
+ frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0)
+
+def make_qrcode_field():
+ """Created QR code Image file"""
+ qr_code_field = dict(
+ fieldname='qr_code',
+ label='QR Code',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1)
+
+ create_custom_field('Sales Invoice', qr_code_field)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
new file mode 100644
index 00000000000..cc6c0af7a56
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -0,0 +1,77 @@
+import io
+import os
+
+import frappe
+from pyqrcode import create as qr_create
+
+from erpnext import get_region
+
+
+def create_qr_code(doc, method):
+ """Create QR Code after inserting Sales Inv
+ """
+
+ region = get_region(doc.company)
+ if region not in ['Saudi Arabia']:
+ return
+
+ # if QR Code field not present, do nothing
+ if not hasattr(doc, 'qr_code'):
+ return
+
+ # Don't create QR Code if it already exists
+ qr_code = doc.get("qr_code")
+ if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
+ return
+
+ meta = frappe.get_meta('Sales Invoice')
+
+ for field in meta.get_image_fields():
+ if field.fieldname == 'qr_code':
+ # Creating public url to print format
+ default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
+
+ # System Language
+ language = frappe.get_system_settings('language')
+
+ # creating qr code for the url
+ url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
+ qr_image = io.BytesIO()
+ url = qr_create(url, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+
+ # making file
+ filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "is_private": 0,
+ "content": qr_image.getvalue(),
+ "attached_to_doctype": doc.get("doctype"),
+ "attached_to_name": doc.get("name"),
+ "attached_to_field": "qr_code"
+ })
+
+ _file.save()
+
+ # assigning to document
+ doc.db_set('qr_code', _file.file_url)
+ doc.notify_update()
+
+ break
+
+
+def delete_qr_code_file(doc, method):
+ """Delete QR Code on deleted sales invoice"""
+
+ region = get_region(doc.company)
+ if region not in ['Saudi Arabia']:
+ return
+
+ if hasattr(doc, 'qr_code'):
+ if doc.get('qr_code'):
+ file_doc = frappe.get_list('File', {
+ 'file_url': doc.get('qr_code')
+ })
+ if len(file_doc):
+ frappe.delete_doc('File', file_doc[0].name)
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/wizard/__init__.py b/erpnext/regional/saudi_arabia/wizard/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/data/__init__.py b/erpnext/regional/saudi_arabia/wizard/data/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
new file mode 100644
index 00000000000..709d65be041
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
@@ -0,0 +1,47 @@
+[
+ {
+ "type": "Sales Account",
+ "accounts": [
+ {
+ "title": "Standard rated Sales",
+ "item_tax_template": "KSA VAT 5%",
+ "account": "VAT 5%"
+ },
+ {
+ "title": "Zero rated domestic sales",
+ "item_tax_template": "KSA VAT Zero",
+ "account": "VAT Zero"
+ },
+ {
+ "title": "Exempted sales",
+ "item_tax_template": "KSA VAT Exempted",
+ "account": "VAT Zero"
+ }
+ ]
+ },
+ {
+ "type": "Purchase Account",
+ "accounts": [
+ {
+ "title": "Standard rated domestic purchases",
+ "item_tax_template": "KSA VAT 5%",
+ "account": "VAT 5%"
+ },
+ {
+ "title": "Imports subject to VAT paid at customs",
+ "item_tax_template": "KSA Excise 50%",
+ "account": "Excise 50%"
+ },
+ {
+ "title": "Zero rated purchases",
+ "item_tax_template": "KSA VAT Zero",
+ "account": "VAT Zero"
+ },
+ {
+ "title": "Exempted purchases",
+ "item_tax_template": "KSA VAT Exempted",
+ "account": "VAT Zero"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py b/erpnext/regional/saudi_arabia/wizard/operations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
new file mode 100644
index 00000000000..3c89edd37ed
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
@@ -0,0 +1,46 @@
+import json
+import os
+
+import frappe
+
+from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
+
+
+def create_ksa_vat_setting(company):
+ """On creation of first company. Creates KSA VAT Setting"""
+
+ company = frappe.get_doc('Company', company)
+ setup_taxes_and_charges(company.name, company.country)
+
+ file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json')
+ with open(file_path, 'r') as json_file:
+ account_data = json.load(json_file)
+
+ # Creating KSA VAT Setting
+ ksa_vat_setting = frappe.get_doc({
+ 'doctype': 'KSA VAT Setting',
+ 'company': company.name
+ })
+
+ for data in account_data:
+ if data['type'] == 'Sales Account':
+ for row in data['accounts']:
+ item_tax_template = row['item_tax_template']
+ account = row['account']
+ ksa_vat_setting.append('ksa_vat_sales_accounts', {
+ 'title': row['title'],
+ 'item_tax_template': f'{item_tax_template} - {company.abbr}',
+ 'account': f'{account} - {company.abbr}'
+ })
+
+ elif data['type'] == 'Purchase Account':
+ for row in data['accounts']:
+ item_tax_template = row['item_tax_template']
+ account = row['account']
+ ksa_vat_setting.append('ksa_vat_purchase_accounts', {
+ 'title': row['title'],
+ 'item_tax_template': f'{item_tax_template} - {company.abbr}',
+ 'account': f'{account} - {company.abbr}'
+ })
+
+ ksa_vat_setting.save()
diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py
index 25982b81227..f7b921a491b 100644
--- a/erpnext/regional/united_states/setup.py
+++ b/erpnext/regional/united_states/setup.py
@@ -16,30 +16,9 @@ def setup(company=None, patch=True):
setup_company_independent_fixtures(patch=patch)
def setup_company_independent_fixtures(company=None, patch=True):
- add_product_tax_categories()
make_custom_fields()
- add_permissions()
- frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
add_print_formats()
-# Product Tax categories imported from taxjar api
-def add_product_tax_categories():
- with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
- tax_categories = json.loads(f.read())
- create_tax_categories(tax_categories['categories'])
-
-def create_tax_categories(data):
- for d in data:
- tax_category = frappe.new_doc('Product Tax Category')
- tax_category.description = d.get("description")
- tax_category.product_tax_code = d.get("product_tax_code")
- tax_category.category_name = d.get("name")
- try:
- tax_category.db_insert()
- except frappe.DuplicateEntryError:
- pass
-
-
def make_custom_fields(update=True):
custom_fields = {
'Supplier': [
@@ -61,29 +40,10 @@ def make_custom_fields(update=True):
'Quotation': [
dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges',
label='Is customer exempted from sales tax?')
- ],
- 'Sales Invoice Item': [
- dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
- label='Product Tax Category', fetch_from='item_code.product_tax_category'),
- dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
- label='Tax Collectable', read_only=1),
- dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
- label='Taxable Amount', read_only=1)
- ],
- 'Item': [
- dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
- label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=update)
-def add_permissions():
- doctype = "Product Tax Category"
- for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
- add_permission(doctype, role, 0)
- update_permission_property(doctype, role, 0, 'write', 1)
- update_permission_property(doctype, role, 0, 'create', 1)
-
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 1961371d0b8..b8b023307f2 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -78,6 +78,8 @@ frappe.ui.form.on("Sales Order", {
});
erpnext.queries.setup_warehouse_query(frm);
+
+ frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
},
delivery_date: function(frm) {
diff --git a/erpnext/setup/doctype/uom/uom.json b/erpnext/setup/doctype/uom/uom.json
index 3a4e7f6dc4b..844a11f1397 100644
--- a/erpnext/setup/doctype/uom/uom.json
+++ b/erpnext/setup/doctype/uom/uom.json
@@ -1,164 +1,82 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:uom_name",
- "beta": 0,
"creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "enabled",
+ "uom_name",
+ "must_be_whole_number"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "uom_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "UOM Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "uom_name",
"oldfieldtype": "Data",
- "permlevel": 0,
- "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": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "Check this to disallow fractions. (for Nos)",
"fieldname": "must_be_whole_number",
"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": "Must be Whole Number",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Must be Whole Number"
+ },
+ {
+ "default": "1",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-compass",
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-29 06:35:56.143361",
+ "links": [],
+ "modified": "2021-10-18 14:07:43.722144",
"modified_by": "Administrator",
"module": "Setup",
"name": "UOM",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Item Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
- "role": "Stock Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Stock Manager"
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Stock User"
}
],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
"show_name_in_global_search": 1,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 6ca3d637da4..db171fa9627 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -1,6 +1,7 @@
{
"category": "Modules",
"charts": [],
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":4}}]",
"creation": "2020-03-12 14:47:51.166455",
"developer_mode_only": 0,
"disable_user_customization": 0,
@@ -15,7 +16,7 @@
"is_standard": 1,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-06-12 01:58:11.399566",
+ "modified": "2021-10-26 21:32:55.323591",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -29,6 +30,14 @@
"link_to": "Projects Settings",
"type": "DocType"
},
+ {
+ "color": "Grey",
+ "doc_view": "",
+ "icon": "dot-horizontal",
+ "label": "Naming Series",
+ "link_to": "Naming Series",
+ "type": "DocType"
+ },
{
"icon": "accounting",
"label": "Accounts Settings",
diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py
index 6080fb4a5fa..6f2a389b707 100644
--- a/erpnext/stock/doctype/item_alternative/item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/item_alternative.py
@@ -25,19 +25,29 @@ class ItemAlternative(Document):
frappe.throw(_("Alternative item must not be same as item code"))
item_meta = frappe.get_meta("Item")
- fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no","has_batch_no"]
- item_data = frappe.db.get_values("Item", self.item_code, fields, as_dict=1)
- alternative_item_data = frappe.db.get_values("Item", self.alternative_item_code, fields, as_dict=1)
+ fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no", "has_batch_no", "allow_alternative_item"]
+ item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1)
+ alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1)
for field in fields:
- if item_data[0].get(field) != alternative_item_data[0].get(field):
+ if item_data.get(field) != alternative_item_data.get(field):
raise_exception, alert = [1, False] if field == "is_stock_item" else [0, True]
frappe.msgprint(_("The value of {0} differs between Items {1} and {2}") \
.format(frappe.bold(item_meta.get_label(field)),
frappe.bold(self.alternative_item_code),
frappe.bold(self.item_code)),
- alert=alert, raise_exception=raise_exception)
+ alert=alert, raise_exception=raise_exception, indicator="Orange")
+
+ alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}")
+
+ if not item_data.allow_alternative_item:
+ frappe.throw(alternate_item_check_msg.format(self.item_code))
+ if self.two_way and not alternative_item_data.allow_alternative_item:
+ frappe.throw(alternate_item_check_msg.format(self.item_code))
+
+
+
def validate_duplicate(self):
if frappe.db.get_value("Item Alternative", {'item_code': self.item_code,
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index 21467935370..c604c711ef5 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -18,7 +18,9 @@
"get_item_locations",
"section_break_6",
"locations",
- "amended_from"
+ "amended_from",
+ "print_settings_section",
+ "group_same_items"
],
"fields": [
{
@@ -110,14 +112,28 @@
"options": "STO-PICK-.YYYY.-",
"reqd": 1,
"set_only_once": 1
+ },
+ {
+ "fieldname": "print_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Print Settings"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "group_same_items",
+ "fieldtype": "Check",
+ "label": "Group Same Items",
+ "print_hide": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-03-17 11:38:41.932875",
+ "modified": "2021-10-05 15:08:40.369957",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -184,4 +200,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index dffbe80fa39..4c02f3db43b 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -2,10 +2,8 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import json
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
import frappe
from frappe import _
@@ -121,6 +119,34 @@ class PickList(Document):
and (self.for_qty is None or self.for_qty == 0):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
+ def before_print(self, settings=None):
+ if self.get("group_same_items"):
+ self.group_similar_items()
+
+ def group_similar_items(self):
+ group_item_qty = defaultdict(float)
+ group_picked_qty = defaultdict(float)
+
+ for item in self.locations:
+ group_item_qty[(item.item_code, item.warehouse)] += item.qty
+ group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty
+
+ duplicate_list = []
+ for item in self.locations:
+ if (item.item_code, item.warehouse) in group_item_qty:
+ item.qty = group_item_qty[(item.item_code, item.warehouse)]
+ item.picked_qty = group_picked_qty[(item.item_code, item.warehouse)]
+ item.stock_qty = group_item_qty[(item.item_code, item.warehouse)]
+ del group_item_qty[(item.item_code, item.warehouse)]
+ else:
+ duplicate_list.append(item)
+
+ for item in duplicate_list:
+ self.remove(item)
+
+ for idx, item in enumerate(self.locations, start=1):
+ item.idx = idx
+
def validate_item_locations(pick_list):
if not pick_list.locations:
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index fd0b3680df2..58b46e1eefc 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
+from frappe import _dict
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
@@ -356,6 +357,39 @@ class TestPickList(ERPNextTestCase):
sales_order.cancel()
purchase_receipt.cancel()
+ def test_pick_list_grouping_before_print(self):
+ def _compare_dicts(a, b):
+ "compare dicts but ignore missing keys in `a`"
+ for key, value in a.items():
+ self.assertEqual(b.get(key), value, msg=f"{key} doesn't match")
+
+ # nothing should be grouped
+ pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
+ _dict(item_code="A", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2),
+ ])
+ pl.before_print()
+ self.assertEqual(len(pl.locations), 4)
+
+ # grouping should halve the number of items
+ pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
+ _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
+ _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
+ _dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
+ ])
+ pl.before_print()
+ self.assertEqual(len(pl.locations), 2)
+
+ expected_items = [
+ _dict(item_code="A", warehouse="X", qty=8, picked_qty=3),
+ _dict(item_code="B", warehouse="Y", qty=6, picked_qty=4),
+ ]
+ for expected_item, created_item in zip(expected_items, pl.locations):
+ _compare_dicts(expected_item, created_item)
+
# def test_pick_list_skips_items_in_expired_batch(self):
# pass
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index cbff2149d64..e0190b64a75 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -89,7 +89,13 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
- out.update(get_bin_details(args.item_code, out.warehouse, args.company))
+ if doc and doc.get('doctype') == 'Purchase Order':
+ # calculate company_total_stock only for po
+ bin_details = get_bin_details(args.item_code, out.warehouse, args.company)
+ else:
+ bin_details = get_bin_details(args.item_code, out.warehouse)
+
+ out.update(bin_details)
# update args with out, if key or value not exists
for key, value in iteritems(out):
@@ -485,8 +491,9 @@ def get_item_tax_template(args, item, out):
"item_tax_template": None
}
"""
- item_tax_template = args.get("item_tax_template")
- item_tax_template = _get_item_tax_template(args, item.taxes, out)
+ item_tax_template = None
+ if item.taxes:
+ item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_group = item.item_group
@@ -502,17 +509,17 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
taxes_with_no_validity = []
for tax in taxes:
- tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company')
- if (tax.valid_from or tax.maximum_net_rate) and tax_company == args['company']:
- # In purchase Invoice first preference will be given to supplier invoice date
- # if supplier date is not present then posting date
- validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
+ tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, 'company')
+ if tax_company == args['company']:
+ if (tax.valid_from or tax.maximum_net_rate):
+ # In purchase Invoice first preference will be given to supplier invoice date
+ # if supplier date is not present then posting date
+ validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
- if getdate(tax.valid_from) <= getdate(validation_date) \
- and is_within_valid_range(args, tax):
- taxes_with_validity.append(tax)
- else:
- if tax_company == args['company']:
+ if getdate(tax.valid_from) <= getdate(validation_date) \
+ and is_within_valid_range(args, tax):
+ taxes_with_validity.append(tax)
+ else:
taxes_with_no_validity.append(tax)
if taxes_with_validity:
@@ -890,8 +897,7 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa
res[fieldname] = pos_profile.get(fieldname)
if res.get("warehouse"):
- res.actual_qty = get_bin_details(args.item_code,
- res.warehouse).get("actual_qty")
+ res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty")
return res
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index fc5d5c12da4..bb53c557371 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -202,7 +202,9 @@ def get_item_warehouse_map(filters, sle):
value_diff = flt(d.stock_value_difference)
- if d.posting_date < from_date:
+ if d.posting_date < from_date or (d.posting_date == from_date
+ and d.voucher_type == "Stock Reconciliation" and
+ frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"):
qty_dict.opening_qty += qty_diff
qty_dict.opening_val += value_diff
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 1ea58fed191..4e20b472617 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -21,7 +21,7 @@ def execute(filters=None):
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom)
- opening_row = get_opening_balance(filters, columns)
+ opening_row = get_opening_balance(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
data = []
@@ -218,7 +218,7 @@ def get_sle_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
-def get_opening_balance(filters, columns):
+def get_opening_balance(filters, columns, sl_entries):
if not (filters.item_code and filters.warehouse and filters.from_date):
return
@@ -230,6 +230,15 @@ def get_opening_balance(filters, columns):
"posting_time": "00:00:00"
})
+ # check if any SLEs are actually Opening Stock Reconciliation
+ for sle in sl_entries:
+ if (sle.get("voucher_type") == "Stock Reconciliation"
+ and sle.get("date").split()[0] == filters.from_date
+ and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock"
+ ):
+ last_entry = sle
+ sl_entries.remove(sle)
+
row = {
"item_code": _("'Opening'"),
"qty_after_transaction": last_entry.get("qty_after_transaction", 0),
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index e8768c4aed3..d903c0b8ece 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -123,12 +123,11 @@ def set_as_cancel(voucher_type, voucher_no):
(now(), frappe.session.user, voucher_type, voucher_no))
def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
- args.update({"doctype": "Stock Ledger Entry"})
+ args["doctype"] = "Stock Ledger Entry"
sle = frappe.get_doc(args)
sle.flags.ignore_permissions = 1
sle.allow_negative_stock=allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
- sle.insert()
sle.submit()
return sle
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index c4a0497b744..e1d5a89082e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -101,11 +101,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
if with_valuation_rate:
if with_serial_no:
- serial_nos = last_entry.get("serial_no")
-
- if (serial_nos and
- len(get_serial_nos_data(serial_nos)) < last_entry.qty_after_transaction):
- serial_nos = get_serial_nos_data_after_transactions(args)
+ serial_nos = get_serial_nos_data_after_transactions(args)
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
if last_entry else (0.0, 0.0, 0.0))
@@ -115,19 +111,32 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
return last_entry.qty_after_transaction if last_entry else 0.0
def get_serial_nos_data_after_transactions(args):
- serial_nos = []
- data = frappe.db.sql(""" SELECT serial_no, actual_qty
- FROM `tabStock Ledger Entry`
- WHERE
- item_code = %(item_code)s and warehouse = %(warehouse)s
- and timestamp(posting_date, posting_time) < timestamp(%(posting_date)s, %(posting_time)s)
- order by posting_date, posting_time asc """, args, as_dict=1)
+ from pypika import CustomFunction
- for d in data:
- if d.actual_qty > 0:
- serial_nos.extend(get_serial_nos_data(d.serial_no))
+ serial_nos = set()
+ args = frappe._dict(args)
+ sle = frappe.qb.DocType('Stock Ledger Entry')
+ Timestamp = CustomFunction('timestamp', ['date', 'time'])
+
+ stock_ledger_entries = frappe.qb.from_(
+ sle
+ ).select(
+ 'serial_no','actual_qty'
+ ).where(
+ (sle.item_code == args.item_code)
+ & (sle.warehouse == args.warehouse)
+ & (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time))
+ & (sle.is_cancelled == 0)
+ ).orderby(
+ sle.posting_date, sle.posting_time, sle.creation
+ ).run(as_dict=1)
+
+ for stock_ledger_entry in stock_ledger_entries:
+ changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
+ if stock_ledger_entry.actual_qty > 0:
+ serial_nos.update(changed_serial_no)
else:
- serial_nos = list(set(serial_nos) - set(get_serial_nos_data(d.serial_no)))
+ serial_nos.difference_update(changed_serial_no)
return '\n'.join(serial_nos)
diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json
index 5d3d3ace59d..bf1daa16f86 100644
--- a/erpnext/support/doctype/support_settings/support_settings.json
+++ b/erpnext/support/doctype/support_settings/support_settings.json
@@ -37,7 +37,6 @@
},
{
"default": "7",
- "description": "Auto close Issue after 7 days",
"fieldname": "close_issue_after_days",
"fieldtype": "Int",
"label": "Close Issue After Days"
@@ -164,7 +163,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-06-11 13:08:38.473616",
+ "modified": "2021-10-14 13:08:38.473616",
"modified_by": "Administrator",
"module": "Support",
"name": "Support Settings",
@@ -185,4 +184,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index 889cf1ab38d..892d62513e2 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -339,15 +339,15 @@
{% for attr_value in attribute.item_attribute_values %}
-
+
- {{ attr_value.attribute_value }}
+ {{ attr_value }}
{% endfor %}
diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html
index 7b3c9a41318..3f2c1f2a1df 100644
--- a/erpnext/templates/includes/order/order_macros.html
+++ b/erpnext/templates/includes/order/order_macros.html
@@ -1,43 +1,49 @@
-{% from "erpnext/templates/includes/macros.html" import product_image_square %}
+{% from "erpnext/templates/includes/macros.html" import product_image %}
{% macro item_name_and_description(d) %}
-
-
-
- {{ product_image_square(d.thumbnail or d.image) }}
-
-
-
- {{ d.item_code }}
-
+
+
+
+ {% if d.thumbnail or d.image %}
+ {{ product_image(d.thumbnail or d.image, no_border=True) }}
+ {% else %}
+
+ {{ frappe.utils.get_abbr(d.item_name) or "NA" }}
+
+ {% endif %}
+
+
+
+ {{ d.item_code }}
+
{{ html2text(d.description) | truncate(140) }}
-
-
+
+
{% endmacro %}
{% macro item_name_and_description_cart(d) %}
-
-
-
- {{ product_image_square(d.thumbnail or d.image) }}
-
-
-
- {{ d.item_name|truncate(25) }}
-
+
+ {{ d.item_name|truncate(25) }}
+
+
+
+ –
+
+
+
+
+ +
+
+
+
+
{% endmacro %}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 19191d89096..a10870db278 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -12,175 +12,173 @@
{% endblock %}
{% block header_actions %}
-
-
- {{ _('Actions') }}
-
-
-
-
-
+
+
+ {{ _('Actions') }}
+
+
+
+
{% endblock %}
{% block page_content %}
-
-
-
-
- {% if doc.doctype == "Quotation" and not doc.docstatus %}
- {{ _("Pending") }}
- {% else %}
- {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
- {% endif %}
-
-
-
- {{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
- {% if doc.valid_till %}
-
- {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
-
- {% endif %}
-
-
-
-
- {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
- {{ party_name }}
-
- {% if doc.contact_display and doc.contact_display != party_name %}
-
- {{ doc.contact_display }}
- {% endif %}
-
-
-{% if doc._header %}
-{{ doc._header }}
-{% endif %}
-
-
-
-
-
-
- {% for d in doc.items %}
-
-
- {{ item_name_and_description(d) }}
-
-
- {{ d.qty }}
- {% if d.delivered_qty is defined and d.delivered_qty != None %}
- {{ _("Delivered") }} {{ d.delivered_qty }}
+
+
+
+ {% if doc.doctype == "Quotation" and not doc.docstatus %}
+ {{ _("Pending") }}
+ {% else %}
+ {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
{% endif %}
-
-
- {{ d.get_formatted("amount") }}
- {{ _("Rate:") }} {{ d.get_formatted("rate") }}
-
-
- {% endfor %}
-
-
-
-
-
- {% include "erpnext/templates/includes/order/order_taxes.html" %}
-
-
-
-
-{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
- or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %}
-
-
-{% endif %}
-
-
-{% if attachments %}
-
-
-
-
- {% for attachment in attachments %}
-
- {{ attachment.file_name }}
+
+ {{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
+ {% if doc.valid_till %}
+
+ {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
- {% endfor %}
+ {% endif %}
-
-{% endif %}
-
-{% if doc.terms %}
-
-{% endif %}
+
+
+ {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
+ {{ party_name }}
+
+ {% if doc.contact_display and doc.contact_display != party_name %}
+
+ {{ doc.contact_display }}
+ {% endif %}
+
+
+ {% if doc._header %}
+ {{ doc._header }}
+ {% endif %}
+
+
+
+
+
+
+ {% for d in doc.items %}
+
+
+ {{ item_name_and_description(d) }}
+
+
+ {{ d.qty }}
+ {% if d.delivered_qty is defined and d.delivered_qty != None %}
+ {{ _("Delivered") }} {{ d.delivered_qty }}
+ {% endif %}
+
+
+ {{ d.get_formatted("amount") }}
+ {{ _("Rate:") }} {{ d.get_formatted("rate") }}
+
+
+ {% endfor %}
+
+
+
+
+
+ {% include "erpnext/templates/includes/order/order_taxes.html" %}
+
+
+
+
+ {% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
+ or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %}
+
+ {% endif %}
+
+
+ {% if attachments %}
+
+ {% endif %}
+
+
+ {% if doc.terms %}
+
+ {% endif %}
{% endblock %}
{% block script %}
diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py
index 881f286baeb..3ce68d89bcd 100644
--- a/erpnext/tests/test_woocommerce.py
+++ b/erpnext/tests/test_woocommerce.py
@@ -12,12 +12,6 @@ from erpnext.erpnext_integrations.connectors.woocommerce_connection import order
class TestWoocommerce(unittest.TestCase):
def setUp(self):
- if not frappe.db.exists('Company', 'Woocommerce'):
- company = frappe.new_doc("Company")
- company.company_name = "Woocommerce"
- company.abbr = "W"
- company.default_currency = "INR"
- company.save()
woo_settings = frappe.get_doc("Woocommerce Settings")
if not woo_settings.secret:
@@ -26,14 +20,14 @@ class TestWoocommerce(unittest.TestCase):
woo_settings.api_consumer_key = "ck_fd43ff5756a6abafd95fadb6677100ce95a758a1"
woo_settings.api_consumer_secret = "cs_94360a1ad7bef7fa420a40cf284f7b3e0788454e"
woo_settings.enable_sync = 1
- woo_settings.company = "Woocommerce"
- woo_settings.tax_account = "Sales Expenses - W"
- woo_settings.f_n_f_account = "Expenses - W"
+ woo_settings.company = "_Test Company"
+ woo_settings.tax_account = "Sales Expenses - _TC"
+ woo_settings.f_n_f_account = "Expenses - _TC"
woo_settings.creation_user = "Administrator"
woo_settings.save(ignore_permissions=True)
def test_sales_order_for_woocommerce(self):
- frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}
+ frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"_Test Company","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}
order()
self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"}))