mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
chore: merge branch 'version-13-hotfix' into 'version-13-pre-release'
This commit is contained in:
38
.github/helper/semgrep_rules/README.md
vendored
38
.github/helper/semgrep_rules/README.md
vendored
@@ -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
|
||||
34
.github/helper/semgrep_rules/report.yml
vendored
34
.github/helper/semgrep_rules/report.yml
vendored
@@ -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
|
||||
6
.github/helper/semgrep_rules/security.py
vendored
6
.github/helper/semgrep_rules/security.py
vendored
@@ -1,6 +0,0 @@
|
||||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
||||
10
.github/helper/semgrep_rules/security.yml
vendored
10
.github/helper/semgrep_rules/security.yml
vendored
@@ -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
|
||||
44
.github/helper/semgrep_rules/translate.js
vendored
44
.github/helper/semgrep_rules/translate.js
vendored
@@ -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])
|
||||
61
.github/helper/semgrep_rules/translate.py
vendored
61
.github/helper/semgrep_rules/translate.py
vendored
@@ -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
|
||||
64
.github/helper/semgrep_rules/translate.yml
vendored
64
.github/helper/semgrep_rules/translate.yml
vendored
@@ -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
|
||||
9
.github/helper/semgrep_rules/ux.js
vendored
9
.github/helper/semgrep_rules/ux.js
vendored
@@ -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") }}. ');
|
||||
30
.github/helper/semgrep_rules/ux.yml
vendored
30
.github/helper/semgrep_rules/ux.yml
vendored
@@ -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
|
||||
20
.github/workflows/linters.yml
vendored
20
.github/workflows/linters.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
{%- if einvoice.ShipDtls -%}
|
||||
{%- set shipping = einvoice.ShipDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Shipping</h5>
|
||||
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||
<p>{{ shipping.Gstin }}</p>
|
||||
<p>{{ shipping.LglNm }}</p>
|
||||
<p>{{ shipping.Addr1 }}</p>
|
||||
@@ -86,6 +86,17 @@
|
||||
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||
<p>{{ buyer.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||
|
||||
{%- if einvoice.DispDtls -%}
|
||||
{%- set dispatch = einvoice.DispDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
|
||||
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
|
||||
<p>{{ dispatch.LglNm }}</p>
|
||||
<p>{{ dispatch.Addr1 }}</p>
|
||||
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
|
||||
<p>{{ dispatch.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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'))
|
||||
) + '<br><br>' + 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
|
||||
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
return discount_filters
|
||||
|
||||
@@ -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)
|
||||
make_website_item(variant, save=True)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> 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)
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
""",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
64
erpnext/manufacturing/report/test_reports.py
Normal file
64
erpnext/manufacturing/report/test_reports.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
31
erpnext/patches/v12_0/update_production_plan_status.py
Normal file
31
erpnext/patches/v12_0/update_production_plan_status.py
Normal file
@@ -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
|
||||
)
|
||||
""")
|
||||
@@ -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)
|
||||
frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True)
|
||||
|
||||
18
erpnext/patches/v13_0/update_category_in_ltds_certificate.py
Normal file
18
erpnext/patches/v13_0/update_category_in_ltds_certificate.py
Normal file
@@ -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()
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
|
||||
"document_name": doc.name,
|
||||
"data": data
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Havenir Solutions and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestKSAVATSalesAccount(unittest.TestCase):
|
||||
pass
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
frappe.listview_settings['KSA VAT Setting'] = {
|
||||
onload () {
|
||||
frappe.breadcrumbs.add('Accounts');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Havenir Solutions and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestKSAVATSetting(unittest.TestCase):
|
||||
pass
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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,
|
||||
|
||||
0
erpnext/regional/report/ksa_vat/__init__.py
Normal file
0
erpnext/regional/report/ksa_vat/__init__.py
Normal file
60
erpnext/regional/report/ksa_vat/ksa_vat.js
Normal file
60
erpnext/regional/report/ksa_vat/ksa_vat.js
Normal file
@@ -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 = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
return value
|
||||
}else if (data.title=='Grand Total'){
|
||||
if (data.title==value) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
return value
|
||||
}else{
|
||||
value = default_formatter(value, row, column, data);
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
console.log($value)
|
||||
return value
|
||||
}
|
||||
}else{
|
||||
value = default_formatter(value, row, column, data);
|
||||
return value;
|
||||
}
|
||||
},
|
||||
};
|
||||
32
erpnext/regional/report/ksa_vat/ksa_vat.json
Normal file
32
erpnext/regional/report/ksa_vat/ksa_vat.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
176
erpnext/regional/report/ksa_vat/ksa_vat.py
Normal file
176
erpnext/regional/report/ksa_vat/ksa_vat.py
Normal file
@@ -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 <a href="{}">KSA VAT Setting</a> 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user