Compare commits
1 Commits
v13.15.2
...
fix-error-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a3a0a83b7 |
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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
Normal file
34
.github/helper/semgrep_rules/report.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
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
Normal file
6
.github/helper/semgrep_rules/security.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
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
Normal file
10
.github/helper/semgrep_rules/security.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
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
Normal file
44
.github/helper/semgrep_rules/translate.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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
Normal file
61
.github/helper/semgrep_rules/translate.py
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# 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
Normal file
64
.github/helper/semgrep_rules/translate.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
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
Normal file
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
// 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
Normal file
30
.github/helper/semgrep_rules/ux.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
||||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
|
||||
20
.github/workflows/linters.yml
vendored
20
.github/workflows/linters.yml
vendored
@@ -10,6 +10,13 @@ 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
|
||||
@@ -17,15 +24,4 @@ jobs:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
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
|
||||
uses: pre-commit/action@v2.0.0
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '13.15.2'
|
||||
__version__ = '13.13.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -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_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
elif len(set(child.keys()) - set(["account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
|
||||
}
|
||||
|
||||
let help_content =
|
||||
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
`<table class="table table-bordered" style="background-color: #f9f9f9;">
|
||||
<tr><td>
|
||||
<p>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
|
||||
@@ -344,15 +344,7 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
||||
|
||||
def get_je_matching_query(amount_condition, transaction):
|
||||
# get matching journal entry query
|
||||
|
||||
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
|
||||
root_type = frappe.get_value("Account", company_account, "root_type")
|
||||
|
||||
if root_type == "Liability":
|
||||
cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
|
||||
else:
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
return f"""
|
||||
|
||||
SELECT
|
||||
|
||||
@@ -6,7 +6,7 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
frappe.ui.form.on('Loyalty Program', {
|
||||
setup: function(frm) {
|
||||
var help_content =
|
||||
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
`<table class="table table-bordered" style="background-color: #f9f9f9;">
|
||||
<tr><td>
|
||||
<h4>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
|
||||
@@ -27,12 +27,10 @@
|
||||
"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",
|
||||
@@ -442,8 +440,7 @@
|
||||
"depends_on": "eval:(doc.paid_from && doc.paid_to)",
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Cheque/Reference No",
|
||||
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
|
||||
"label": "Cheque/Reference No"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
@@ -455,7 +452,6 @@
|
||||
"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
|
||||
},
|
||||
{
|
||||
@@ -711,30 +707,15 @@
|
||||
"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-10-22 17:50:24.632806",
|
||||
"modified": "2021-07-09 08:58:15.008761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -389,7 +389,7 @@ class PaymentEntry(AccountsController):
|
||||
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
|
||||
|
||||
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
|
||||
for key, allocated_amount in iteritems(invoice_payment_amount_map):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
|
||||
|
||||
@@ -407,7 +407,7 @@ class PaymentEntry(AccountsController):
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
|
||||
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
frappe.db.sql("""
|
||||
@@ -1053,6 +1053,12 @@ def get_outstanding_reference_documents(args):
|
||||
party_account_currency = get_account_currency(args.get("party_account"))
|
||||
company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), args.get("company"), party_account_currency, company_currency)
|
||||
|
||||
# Get positive outstanding sales /purchase invoices/ Fees
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
@@ -1099,12 +1105,6 @@ def get_outstanding_reference_documents(args):
|
||||
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
|
||||
args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), party_account_currency, company_currency, condition=condition)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
if not data:
|
||||
@@ -1137,26 +1137,22 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
|
||||
'invoice_amount': flt(d.invoice_amount),
|
||||
'outstanding_amount': flt(d.outstanding_amount),
|
||||
'payment_amount': payment_term.payment_amount,
|
||||
'payment_term': payment_term.payment_term
|
||||
'payment_term': payment_term.payment_term,
|
||||
'allocated_amount': payment_term.outstanding
|
||||
}))
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = ref[0]['voucher_no']
|
||||
voucher_type = ref[0]['voucher_type']
|
||||
voucher_no = outstanding_invoices[idx]['voucher_no']
|
||||
voucher_type = outstanding_invoices[idx]['voucher_type']
|
||||
|
||||
frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
|
||||
voucher_type, voucher_no, len(ref)), alert=True)
|
||||
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
outstanding_invoices.pop(idx - 1)
|
||||
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
return outstanding_invoices
|
||||
|
||||
def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
company, party_account_currency, company_currency, cost_center=None, filters=None):
|
||||
@@ -1223,7 +1219,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
return order_list
|
||||
|
||||
def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
party_account_currency, company_currency, cost_center=None, condition=None):
|
||||
company, party_account_currency, company_currency, cost_center=None):
|
||||
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
supplier_condition = ""
|
||||
if voucher_type == "Purchase Invoice":
|
||||
@@ -1245,21 +1241,19 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s and {party_account} = %s and docstatus = 1 and
|
||||
outstanding_amount < 0
|
||||
company = %s and outstanding_amount < 0
|
||||
{supplier_condition}
|
||||
{condition}
|
||||
order by
|
||||
posting_date, name
|
||||
""".format(**{
|
||||
"supplier_condition": supplier_condition,
|
||||
"condition": condition,
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"party_account": "debit_to" if party_type == "Customer" else "credit_to",
|
||||
"cost_center": cost_center
|
||||
}), (party, party_account), as_dict=True)
|
||||
}), (party, party_account, company), as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -10,9 +10,6 @@ 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) {
|
||||
|
||||
@@ -4,14 +4,9 @@
|
||||
frappe.provide("erpnext.accounts");
|
||||
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
|
||||
onload: function() {
|
||||
const default_company = frappe.defaults.get_default('company');
|
||||
this.frm.set_value('company', default_company);
|
||||
var me = this;
|
||||
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
this.frm.set_query("party_type", () => {
|
||||
this.frm.set_query("party_type", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
@@ -19,30 +14,44 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
}
|
||||
});
|
||||
|
||||
this.frm.set_query('receivable_payable_account', () => {
|
||||
this.frm.set_query('receivable_payable_account', function() {
|
||||
check_mandatory(me.frm);
|
||||
return {
|
||||
filters: {
|
||||
"company": this.frm.doc.company,
|
||||
"company": me.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
|
||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('bank_cash_account', () => {
|
||||
this.frm.set_query('bank_cash_account', function() {
|
||||
check_mandatory(me.frm, true);
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', this.frm.doc.company],
|
||||
['Account', 'company', '=', me.frm.doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
var check_mandatory = (frm, only_company=false) => {
|
||||
var title = __("Mandatory");
|
||||
if (only_company && !frm.doc.company) {
|
||||
frappe.throw({message: __("Please Select a Company First"), title: title});
|
||||
} else if (!frm.doc.company || !frm.doc.party_type) {
|
||||
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
this.frm.disable_save();
|
||||
|
||||
this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
|
||||
@@ -76,92 +85,76 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
},
|
||||
|
||||
company: function() {
|
||||
this.frm.set_value('party', '');
|
||||
var me = this;
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
},
|
||||
|
||||
party_type: function() {
|
||||
this.frm.set_value('party', '');
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.clear_table("invoices");
|
||||
me.frm.clear_table("payments");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.trigger('party');
|
||||
},
|
||||
|
||||
party: function() {
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
this.frm.trigger("clear_child_tables");
|
||||
|
||||
if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) {
|
||||
var me = this;
|
||||
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party
|
||||
company: me.frm.doc.company,
|
||||
party_type: me.frm.doc.party_type,
|
||||
party: me.frm.doc.party
|
||||
},
|
||||
callback: (r) => {
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
me.frm.set_value("receivable_payable_account", r.message);
|
||||
}
|
||||
this.frm.refresh();
|
||||
|
||||
me.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
receivable_payable_account: function() {
|
||||
this.frm.trigger("clear_child_tables");
|
||||
this.frm.refresh();
|
||||
},
|
||||
|
||||
clear_child_tables: function() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh_fields();
|
||||
},
|
||||
|
||||
get_unreconciled_entries: function() {
|
||||
this.frm.clear_table("allocation");
|
||||
var me = this;
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
doc: me.frm.doc,
|
||||
method: 'get_unreconciled_entries',
|
||||
callback: () => {
|
||||
if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Invoices and Payments found for this party and account")});
|
||||
} else if (!(this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Outstanding Invoices found for this party")});
|
||||
} else if (!(this.frm.doc.payments.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Payments found for this party")});
|
||||
callback: function(r, rt) {
|
||||
if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No invoice and payment records found for this party")});
|
||||
}
|
||||
this.frm.refresh();
|
||||
me.frm.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
allocate: function() {
|
||||
let payments = this.frm.fields_dict.payments.grid.get_selected_children();
|
||||
var me = this;
|
||||
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
|
||||
if (!(payments.length)) {
|
||||
payments = this.frm.doc.payments;
|
||||
payments = me.frm.doc.payments;
|
||||
}
|
||||
let invoices = this.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
let invoices = me.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
if (!(invoices.length)) {
|
||||
invoices = this.frm.doc.invoices;
|
||||
invoices = me.frm.doc.invoices;
|
||||
}
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
return me.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: 'allocate_entries',
|
||||
args: {
|
||||
payments: payments,
|
||||
invoices: invoices
|
||||
},
|
||||
callback: () => {
|
||||
this.frm.refresh();
|
||||
callback: function() {
|
||||
me.frm.refresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reconcile: function() {
|
||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
var me = this;
|
||||
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
|
||||
if (show_dialog && show_dialog.length) {
|
||||
|
||||
@@ -193,10 +186,10 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
label: __("Difference Account"),
|
||||
fieldname: 'difference_account',
|
||||
reqd: 1,
|
||||
get_query: () => {
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
company: me.frm.doc.company,
|
||||
is_group: 0
|
||||
}
|
||||
}
|
||||
@@ -210,7 +203,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
}]
|
||||
},
|
||||
],
|
||||
primary_action: () => {
|
||||
primary_action: function() {
|
||||
const args = dialog.get_values()["allocation"];
|
||||
|
||||
args.forEach(d => {
|
||||
@@ -218,7 +211,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
"difference_account", d.difference_account);
|
||||
});
|
||||
|
||||
this.reconcile_payment_entries();
|
||||
me.reconcile_payment_entries();
|
||||
dialog.hide();
|
||||
},
|
||||
primary_action_label: __('Reconcile Entries')
|
||||
@@ -244,12 +237,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
},
|
||||
|
||||
reconcile_payment_entries: function() {
|
||||
var me = this;
|
||||
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
doc: me.frm.doc,
|
||||
method: 'reconcile',
|
||||
callback: () => {
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh();
|
||||
callback: function(r, rt) {
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,7 +180,8 @@
|
||||
"fieldname": "pos_transactions",
|
||||
"fieldtype": "Table",
|
||||
"label": "POS Transactions",
|
||||
"options": "POS Invoice Reference"
|
||||
"options": "POS Invoice Reference",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_opening_entry",
|
||||
@@ -228,7 +229,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-20 16:19:25.340565",
|
||||
"modified": "2021-05-05 16:59:49.723261",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
||||
@@ -113,15 +113,7 @@ 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
|
||||
|
||||
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
|
||||
|
||||
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
|
||||
|
||||
@@ -155,8 +147,6 @@ class POSInvoiceMergeLog(Document):
|
||||
found = True
|
||||
if not found:
|
||||
tax.charge_type = 'Actual'
|
||||
tax.idx = idx
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
@@ -172,11 +162,6 @@ 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.base_rounding_adjustment
|
||||
base_rounded_total += doc.base_rounded_total
|
||||
|
||||
|
||||
if loyalty_points_sum:
|
||||
invoice.redeem_loyalty_points = 1
|
||||
@@ -186,10 +171,6 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.set('items', items)
|
||||
invoice.set('payments', payments)
|
||||
invoice.set('taxes', taxes)
|
||||
invoice.set('rounding_adjustment',rounding_adjustment)
|
||||
invoice.set('base_rounding_adjustment',base_rounding_adjustment)
|
||||
invoice.set('rounded_total',rounded_total)
|
||||
invoice.set('base_rounded_total',base_rounded_total)
|
||||
invoice.additional_discount_percentage = 0
|
||||
invoice.discount_amount = 0.0
|
||||
invoice.taxes_and_charges = None
|
||||
@@ -265,10 +246,7 @@ 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'))
|
||||
if frappe.flags.in_test and not invoices:
|
||||
invoices = get_all_unconsolidated_invoices()
|
||||
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
|
||||
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||
|
||||
if len(invoices) >= 10 and closing_entry:
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
{
|
||||
"fieldname": "payments",
|
||||
"fieldtype": "Table",
|
||||
"label": "Payment Methods",
|
||||
"options": "POS Payment Method",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -378,7 +377,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-14 14:17:00.469298",
|
||||
"modified": "2021-02-01 13:52:51.081311",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -38,7 +38,7 @@ frappe.ui.form.on('Pricing Rule', {
|
||||
|
||||
refresh: function(frm) {
|
||||
var help_content =
|
||||
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
`<table class="table table-bordered" style="background-color: #f9f9f9;">
|
||||
<tr><td>
|
||||
<h4>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
|
||||
@@ -19,7 +19,6 @@ from erpnext.stock.get_item_details import get_item_details
|
||||
class TestPricingRule(unittest.TestCase):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
|
||||
def tearDown(self):
|
||||
delete_existing_pricing_rules()
|
||||
@@ -546,75 +545,6 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||
item.delete()
|
||||
|
||||
def test_pricing_rule_for_different_currency(self):
|
||||
make_item("Test Sanitizer Item")
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Sanitizer Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{
|
||||
"item_code": "Test Sanitizer Item",
|
||||
}],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Rate",
|
||||
"rate": 0,
|
||||
"priority": 2,
|
||||
"margin_type": "Percentage",
|
||||
"margin_rate_or_amount": 0.0,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.rate_or_discount = 'Rate'
|
||||
rule.rate = 100.0
|
||||
rule.insert()
|
||||
|
||||
rule1 = frappe.get_doc(pricing_rule_record)
|
||||
rule1.currency = 'USD'
|
||||
rule1.rate_or_discount = 'Rate'
|
||||
rule1.rate = 2.0
|
||||
rule1.priority = 1
|
||||
rule1.insert()
|
||||
|
||||
args = frappe._dict({
|
||||
"item_code": "Test Sanitizer Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "USD",
|
||||
"doctype": "Sales Invoice",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
"transaction_date": frappe.utils.nowdate()
|
||||
})
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.price_list_rate, 2.0)
|
||||
|
||||
|
||||
args = frappe._dict({
|
||||
"item_code": "Test Sanitizer Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "INR",
|
||||
"doctype": "Sales Invoice",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
"transaction_date": frappe.utils.nowdate()
|
||||
})
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.price_list_rate, 100.0)
|
||||
|
||||
def test_pricing_rule_for_transaction(self):
|
||||
make_item("Water Flask 1")
|
||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
||||
@@ -631,8 +561,6 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -679,13 +607,6 @@ def make_pricing_rule(**args):
|
||||
if args.get(applicable_for):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Campaign',
|
||||
'campaign_name': '_Test Campaign',
|
||||
'name': '_Test Campaign'
|
||||
}).insert()
|
||||
|
||||
def delete_existing_pricing_rules():
|
||||
for doctype in ["Pricing Rule", "Pricing Rule Item Code",
|
||||
|
||||
@@ -29,9 +29,6 @@ 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):
|
||||
@@ -265,11 +262,6 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
else:
|
||||
p.variant_of = None
|
||||
|
||||
if len(pricing_rules) > 1:
|
||||
filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
|
||||
if filtered_rules:
|
||||
pricing_rules = filtered_rules
|
||||
|
||||
# find pricing rule with highest priority
|
||||
if pricing_rules:
|
||||
max_priority = max(cint(p.priority) for p in pricing_rules)
|
||||
|
||||
@@ -22,9 +22,6 @@ price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discoun
|
||||
product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
|
||||
'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
|
||||
|
||||
class TransactionExists(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
class PromotionalScheme(Document):
|
||||
def validate(self):
|
||||
if not self.selling and not self.buying:
|
||||
@@ -33,40 +30,6 @@ class PromotionalScheme(Document):
|
||||
or self.product_discount_slabs):
|
||||
frappe.throw(_("Price or product discount slabs are required"))
|
||||
|
||||
self.validate_applicable_for()
|
||||
self.validate_pricing_rules()
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if self.applicable_for:
|
||||
applicable_for = frappe.scrub(self.applicable_for)
|
||||
|
||||
if not self.get(applicable_for):
|
||||
msg = (f'The field {frappe.bold(self.applicable_for)} is required')
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def validate_pricing_rules(self):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
transaction_exists = False
|
||||
docnames = []
|
||||
|
||||
# If user has changed applicable for
|
||||
if self._doc_before_save.applicable_for == self.applicable_for:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all('Pricing Rule',
|
||||
filters= {'promotional_scheme': self.name})
|
||||
|
||||
for docname in docnames:
|
||||
if frappe.db.exists('Pricing Rule Detail',
|
||||
{'pricing_rule': docname.name, 'docstatus': ('<', 2)}):
|
||||
raise_for_transaction_exists(self.name)
|
||||
|
||||
if docnames and not transaction_exists:
|
||||
for docname in docnames:
|
||||
frappe.delete_doc('Pricing Rule', docname.name)
|
||||
|
||||
def on_update(self):
|
||||
pricing_rules = frappe.get_all(
|
||||
'Pricing Rule',
|
||||
@@ -106,13 +69,6 @@ class PromotionalScheme(Document):
|
||||
{'promotional_scheme': self.name}):
|
||||
frappe.delete_doc('Pricing Rule', rule.name)
|
||||
|
||||
def raise_for_transaction_exists(name):
|
||||
msg = (f"""You can't change the {frappe.bold(_('Applicable For'))}
|
||||
because transactions are present against the Promotional Scheme {frappe.bold(name)}. """)
|
||||
msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.'
|
||||
|
||||
frappe.throw(_(msg), TransactionExists)
|
||||
|
||||
def get_pricing_rules(doc, rules=None):
|
||||
if rules is None:
|
||||
rules = {}
|
||||
@@ -130,59 +86,45 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
|
||||
new_doc = []
|
||||
args = get_args_for_pricing_rule(doc)
|
||||
applicable_for = frappe.scrub(doc.get('applicable_for'))
|
||||
|
||||
for idx, d in enumerate(doc.get(child_doc)):
|
||||
if d.name in rules:
|
||||
if not args.get(applicable_for):
|
||||
docname = get_pricing_rule_docname(d)
|
||||
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname)
|
||||
new_doc.append(pr)
|
||||
else:
|
||||
for applicable_for_value in args.get(applicable_for):
|
||||
docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value)
|
||||
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
|
||||
d, docname, applicable_for, applicable_for_value)
|
||||
new_doc.append(pr)
|
||||
for applicable_for_value in args.get(applicable_for):
|
||||
temp_args = args.copy()
|
||||
docname = frappe.get_all(
|
||||
'Pricing Rule',
|
||||
fields = ["promotional_scheme_id", "name", applicable_for],
|
||||
filters = {
|
||||
'promotional_scheme_id': d.name,
|
||||
applicable_for: applicable_for_value
|
||||
}
|
||||
)
|
||||
|
||||
elif args.get(applicable_for):
|
||||
if docname:
|
||||
pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
|
||||
temp_args[applicable_for] = applicable_for_value
|
||||
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||
else:
|
||||
pr = frappe.new_doc("Pricing Rule")
|
||||
pr.title = doc.name
|
||||
temp_args[applicable_for] = applicable_for_value
|
||||
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||
|
||||
new_doc.append(pr)
|
||||
|
||||
else:
|
||||
applicable_for_values = args.get(applicable_for) or []
|
||||
for applicable_for_value in applicable_for_values:
|
||||
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
|
||||
d, applicable_for=applicable_for, value= applicable_for_value)
|
||||
|
||||
pr = frappe.new_doc("Pricing Rule")
|
||||
pr.title = doc.name
|
||||
temp_args = args.copy()
|
||||
temp_args[applicable_for] = applicable_for_value
|
||||
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||
new_doc.append(pr)
|
||||
else:
|
||||
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d)
|
||||
new_doc.append(pr)
|
||||
|
||||
return new_doc
|
||||
|
||||
def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str:
|
||||
fields = ['promotional_scheme_id', 'name']
|
||||
filters = {
|
||||
'promotional_scheme_id': row.name
|
||||
}
|
||||
|
||||
if applicable_for:
|
||||
fields.append(applicable_for)
|
||||
filters[applicable_for] = applicable_for_value
|
||||
|
||||
docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters)
|
||||
return docname[0].name if docname else ''
|
||||
|
||||
def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None):
|
||||
if docname:
|
||||
pr = frappe.get_doc("Pricing Rule", docname)
|
||||
else:
|
||||
pr = frappe.new_doc("Pricing Rule")
|
||||
|
||||
pr.title = doc.name
|
||||
temp_args = args.copy()
|
||||
|
||||
if value:
|
||||
temp_args[applicable_for] = value
|
||||
|
||||
return set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||
|
||||
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||
pr.update(args)
|
||||
@@ -205,7 +147,6 @@ def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||
apply_on: d.get(apply_on),
|
||||
'uom': d.uom
|
||||
})
|
||||
|
||||
return pr
|
||||
|
||||
def get_args_for_pricing_rule(doc):
|
||||
|
||||
@@ -7,17 +7,10 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestPromotionalScheme(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if frappe.db.exists('Promotional Scheme', '_Test Scheme'):
|
||||
frappe.delete_doc('Promotional Scheme', '_Test Scheme')
|
||||
|
||||
def test_promotional_scheme(self):
|
||||
ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer')
|
||||
ps = make_promotional_scheme()
|
||||
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
|
||||
filters = {'promotional_scheme': ps.name})
|
||||
self.assertTrue(len(price_rules),1)
|
||||
@@ -48,62 +41,22 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
filters = {'promotional_scheme': ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_promotional_scheme_without_applicable_for(self):
|
||||
ps = make_promotional_scheme()
|
||||
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
|
||||
|
||||
self.assertTrue(len(price_rules), 1)
|
||||
frappe.delete_doc('Promotional Scheme', ps.name)
|
||||
|
||||
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_change_applicable_for_in_promotional_scheme(self):
|
||||
ps = make_promotional_scheme()
|
||||
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
|
||||
self.assertTrue(len(price_rules), 1)
|
||||
|
||||
so = make_sales_order(qty=5, currency='USD', do_not_save=True)
|
||||
so.set_missing_values()
|
||||
so.save()
|
||||
self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule)
|
||||
|
||||
ps.applicable_for = 'Customer'
|
||||
ps.append('customer', {
|
||||
'customer': '_Test Customer'
|
||||
})
|
||||
|
||||
self.assertRaises(TransactionExists, ps.save)
|
||||
|
||||
frappe.delete_doc('Sales Order', so.name)
|
||||
frappe.delete_doc('Promotional Scheme', ps.name)
|
||||
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
def make_promotional_scheme():
|
||||
ps = frappe.new_doc('Promotional Scheme')
|
||||
ps.name = '_Test Scheme'
|
||||
ps.append('items',{
|
||||
'item_code': '_Test Item'
|
||||
})
|
||||
|
||||
ps.selling = 1
|
||||
ps.append('price_discount_slabs',{
|
||||
'min_qty': 4,
|
||||
'validate_applied_rule': 0,
|
||||
'discount_percentage': 20,
|
||||
'rule_description': 'Test'
|
||||
})
|
||||
|
||||
ps.company = '_Test Company'
|
||||
if args.applicable_for:
|
||||
ps.applicable_for = args.applicable_for
|
||||
ps.append(frappe.scrub(args.applicable_for), {
|
||||
frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))
|
||||
})
|
||||
|
||||
ps.applicable_for = 'Customer'
|
||||
ps.append('customer',{
|
||||
'customer': "_Test Customer"
|
||||
})
|
||||
ps.save()
|
||||
|
||||
return ps
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"label": "Threshold for Suggestion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "validate_applied_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Applied Rule"
|
||||
@@ -169,7 +169,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-16 00:25:33.843996",
|
||||
"modified": "2021-08-19 15:49:29.598727",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Price Discount",
|
||||
|
||||
@@ -590,20 +590,5 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method:
|
||||
"erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
party_type: 'Supplier',
|
||||
party: frm.doc.supplier,
|
||||
company: frm.doc.company
|
||||
},
|
||||
callback: (response) => {
|
||||
if (response) frm.set_value("credit_to", response.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,26 +10,9 @@ 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.call({
|
||||
method:
|
||||
"erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
party_type: 'Customer',
|
||||
party: this.frm.doc.customer,
|
||||
company: this.frm.doc.company
|
||||
},
|
||||
callback: (response) => {
|
||||
if (response) me.frm.set_value("debit_to", response.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function() {
|
||||
var me = this;
|
||||
this._super();
|
||||
|
||||
@@ -2026,23 +2026,22 @@ 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.mop
|
||||
payment.mode_of_payment = payment_mode.parent
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
doc.set('payments', [])
|
||||
invalid_modes = []
|
||||
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)
|
||||
for pos_payment_method in pos_profile.get('payments'):
|
||||
pos_payment_method = pos_payment_method.as_dict()
|
||||
|
||||
for row in pos_profile.get('payments'):
|
||||
payment_mode = mode_of_payments_info.get(row.mode_of_payment)
|
||||
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
||||
if not payment_mode:
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
|
||||
continue
|
||||
|
||||
payment_mode.default = row.default
|
||||
append_payment(payment_mode)
|
||||
payment_mode[0].default = pos_payment_method.default
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
@@ -2058,24 +2057,6 @@ 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
|
||||
|
||||
@@ -498,11 +498,9 @@ class Subscription(Document):
|
||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||
return getdate() >= getdate(self.current_invoice_start)
|
||||
|
||||
def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
|
||||
def is_current_invoice_generated(self):
|
||||
invoice = self.get_current_invoice()
|
||||
|
||||
if not (_current_start_date and _current_end_date):
|
||||
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
|
||||
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
|
||||
|
||||
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
|
||||
return True
|
||||
@@ -518,16 +516,13 @@ class Subscription(Document):
|
||||
2. Change the `Subscription` status to 'Past Due Date'
|
||||
3. Change the `Subscription` status to 'Cancelled'
|
||||
"""
|
||||
|
||||
if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
|
||||
self.cancel_subscription_at_period_end()
|
||||
|
||||
@@ -561,10 +556,8 @@ class Subscription(Document):
|
||||
self.set_status_grace_period()
|
||||
|
||||
# Generate invoices periodically even if current invoice are unpaid
|
||||
if self.generate_new_invoices_past_due_date and not \
|
||||
self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
|
||||
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
|
||||
or self.is_prepaid_to_invoice()):
|
||||
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||
self.generate_invoice(prorate)
|
||||
|
||||
|
||||
@@ -49,24 +49,15 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
pan_no = ''
|
||||
parties = []
|
||||
party_type, party = get_party_details(inv)
|
||||
has_pan_field = frappe.get_meta(party_type).has_field("pan")
|
||||
|
||||
if not tax_withholding_category:
|
||||
if has_pan_field:
|
||||
fields = ['tax_withholding_category', 'pan']
|
||||
else:
|
||||
fields = ['tax_withholding_category']
|
||||
|
||||
tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1)
|
||||
|
||||
tax_withholding_category = tax_withholding_details.get('tax_withholding_category')
|
||||
pan_no = tax_withholding_details.get('pan')
|
||||
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
|
||||
|
||||
if not tax_withholding_category:
|
||||
return
|
||||
|
||||
# if tax_withholding_category passed as an argument but not pan_no
|
||||
if not pan_no and has_pan_field:
|
||||
if not pan_no:
|
||||
pan_no = frappe.db.get_value(party_type, party, 'pan')
|
||||
|
||||
# Get others suppliers with the same PAN No
|
||||
@@ -174,7 +165,6 @@ 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')
|
||||
|
||||
@@ -85,8 +85,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
|
||||
if party_type=="Customer":
|
||||
party_details["sales_team"] = [{
|
||||
"sales_person": d.sales_person,
|
||||
"allocated_percentage": d.allocated_percentage or None,
|
||||
"commission_rate": d.commission_rate
|
||||
"allocated_percentage": d.allocated_percentage or None
|
||||
} for d in party.get("sales_team")]
|
||||
|
||||
# supplier tax withholding category
|
||||
@@ -221,7 +220,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
|
||||
return out
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_account(party_type, party=None, company=None):
|
||||
def get_party_account(party_type, party, company=None):
|
||||
"""Returns the account for the given `party`.
|
||||
Will first search in party (Customer / Supplier) record, if not found,
|
||||
will search in group (Customer Group / Supplier Group),
|
||||
@@ -229,11 +228,8 @@ def get_party_account(party_type, party=None, company=None):
|
||||
if not company:
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
if not party and party_type in ['Customer', 'Supplier']:
|
||||
default_account_name = "default_receivable_account" \
|
||||
if party_type=="Customer" else "default_payable_account"
|
||||
|
||||
return frappe.get_cached_value('Company', company, default_account_name)
|
||||
if not party:
|
||||
return
|
||||
|
||||
account = frappe.db.get_value("Party Account",
|
||||
{"parenttype": party_type, "parent": party, "company": company}, "account")
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
{%- if einvoice.ShipDtls -%}
|
||||
{%- set shipping = einvoice.ShipDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||
<h5 style="margin-bottom: 5px;">Shipping</h5>
|
||||
<p>{{ shipping.Gstin }}</p>
|
||||
<p>{{ shipping.LglNm }}</p>
|
||||
<p>{{ shipping.Addr1 }}</p>
|
||||
@@ -86,17 +86,6 @@
|
||||
{%- 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,9 +114,8 @@ 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]:
|
||||
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)
|
||||
account_name = get_root_account_name(data[0].root_type, company)
|
||||
opening_value += get_opening_balance(account_name, data, company)
|
||||
|
||||
opening_balance[company] = opening_value
|
||||
|
||||
|
||||
@@ -425,7 +425,8 @@ def set_gl_entries_by_account(
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and is_cancelled = 0
|
||||
{distributed_cost_center_query}""".format(
|
||||
{distributed_cost_center_query}
|
||||
order by account, posting_date""".format(
|
||||
additional_conditions=additional_conditions,
|
||||
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, _dict
|
||||
from frappe.utils import cstr, getdate
|
||||
from frappe.utils import cstr, flt, getdate
|
||||
from six import iteritems
|
||||
|
||||
from erpnext import get_company_currency, get_default_company
|
||||
@@ -19,8 +19,6 @@ from erpnext.accounts.report.financial_statements import get_cost_centers_with_c
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
|
||||
# to cache translations
|
||||
TRANSLATIONS = frappe._dict()
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
@@ -46,20 +44,10 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
|
||||
update_translations()
|
||||
|
||||
res = get_result(filters, account_details)
|
||||
|
||||
return columns, res
|
||||
|
||||
def update_translations():
|
||||
TRANSLATIONS.update(
|
||||
dict(
|
||||
OPENING = _('Opening'),
|
||||
TOTAL = _('Total'),
|
||||
CLOSING_TOTAL = _('Closing (Opening + Total)')
|
||||
)
|
||||
)
|
||||
|
||||
def validate_filters(filters, account_details):
|
||||
if not filters.get("company"):
|
||||
@@ -167,8 +155,6 @@ 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",
|
||||
@@ -365,9 +351,9 @@ def get_totals_dict():
|
||||
credit_in_account_currency=0.0
|
||||
)
|
||||
return _dict(
|
||||
opening = _get_debit_credit_dict(TRANSLATIONS.OPENING),
|
||||
total = _get_debit_credit_dict(TRANSLATIONS.TOTAL),
|
||||
closing = _get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL)
|
||||
opening = _get_debit_credit_dict(_('Opening')),
|
||||
total = _get_debit_credit_dict(_('Total')),
|
||||
closing = _get_debit_credit_dict(_('Closing (Opening + Total)'))
|
||||
)
|
||||
|
||||
def group_by_field(group_by):
|
||||
@@ -392,23 +378,22 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
entries = []
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get('group_by'))
|
||||
group_by_voucher_consolidated = filters.get("group_by") == 'Group by Voucher (Consolidated)'
|
||||
|
||||
if filters.get('show_net_values_in_party_account'):
|
||||
account_type_map = get_account_type_map(filters.get('company'))
|
||||
|
||||
def update_value_in_dict(data, key, gle):
|
||||
data[key].debit += gle.debit
|
||||
data[key].credit += gle.credit
|
||||
data[key].debit += flt(gle.debit)
|
||||
data[key].credit += flt(gle.credit)
|
||||
|
||||
data[key].debit_in_account_currency += gle.debit_in_account_currency
|
||||
data[key].credit_in_account_currency += gle.credit_in_account_currency
|
||||
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||
|
||||
if filters.get('show_net_values_in_party_account') and \
|
||||
account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
|
||||
net_value = data[key].debit - data[key].credit
|
||||
net_value_in_account_currency = data[key].debit_in_account_currency \
|
||||
- data[key].credit_in_account_currency
|
||||
net_value = flt(data[key].debit) - flt(data[key].credit)
|
||||
net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
|
||||
- flt(data[key].credit_in_account_currency)
|
||||
|
||||
if net_value < 0:
|
||||
dr_or_cr = 'credit'
|
||||
@@ -426,29 +411,19 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
data[key].against_voucher += ', ' + gle.against_voucher
|
||||
|
||||
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
|
||||
show_opening_entries = filters.get("show_opening_entries")
|
||||
|
||||
for gle in gl_entries:
|
||||
group_by_value = gle.get(group_by)
|
||||
|
||||
if (gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries)):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, 'opening', gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle)
|
||||
|
||||
if (gle.posting_date < from_date or
|
||||
(cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries"))):
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'opening', gle)
|
||||
update_value_in_dict(totals, 'opening', gle)
|
||||
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, 'total', gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'total', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
gle_map[group_by_value].entries.append(gle)
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
gle_map[gle.get(group_by)].entries.append(gle)
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
@@ -460,7 +435,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
for key, value in consolidated_gle.items():
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
|
||||
update_value_in_dict(totals, 'total', value)
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
|
||||
update_value_in_dict(totals, 'closing', value)
|
||||
entries.append(value)
|
||||
|
||||
|
||||
@@ -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, {}).get('pan'),
|
||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
|
||||
'supplier': supplier_map.get(supplier).name
|
||||
}
|
||||
|
||||
if filters.naming_series == 'Naming Series':
|
||||
row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')})
|
||||
row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
|
||||
|
||||
row.update({
|
||||
'section_code': tax_withholding_category,
|
||||
'entity_type': supplier_map.get(supplier, {}).get('supplier_type'),
|
||||
'entity_type': supplier_map.get(supplier).supplier_type,
|
||||
'tds_rate': rate,
|
||||
'total_amount_credited': total_amount_credited,
|
||||
'tds_deducted': tds_deducted,
|
||||
|
||||
@@ -450,8 +450,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# new row with references
|
||||
new_row = journal_entry.append("accounts")
|
||||
|
||||
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
|
||||
new_row.update(jv_detail.as_dict().copy())
|
||||
|
||||
new_row.set(d["dr_or_cr"], d["allocated_amount"])
|
||||
new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',
|
||||
@@ -580,10 +579,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_company_default(company, fieldname, ignore_validation=False):
|
||||
value = frappe.get_cached_value('Company', company, fieldname)
|
||||
def get_company_default(company, fieldname):
|
||||
value = frappe.get_cached_value('Company', company, fieldname)
|
||||
|
||||
if not ignore_validation and not value:
|
||||
if not value:
|
||||
throw(_("Please set default {0} in Company {1}")
|
||||
.format(frappe.get_meta("Company").get_label(fieldname), company))
|
||||
|
||||
|
||||
@@ -454,17 +454,6 @@
|
||||
"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,
|
||||
@@ -1045,16 +1034,6 @@
|
||||
"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,
|
||||
@@ -1103,7 +1082,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-26 13:15:52.872470",
|
||||
"modified": "2021-08-23 16:06:34.167267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
|
||||
@@ -16,8 +16,9 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldname:"status",
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
options: "\nIn Location\nDisposed",
|
||||
default: 'In Location'
|
||||
options: "In Location\nDisposed",
|
||||
default: 'In Location',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
|
||||
@@ -45,13 +45,12 @@ def get_conditions(filters):
|
||||
if filters.get('cost_center'):
|
||||
conditions["cost_center"] = filters.get('cost_center')
|
||||
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
operand = 'not in'
|
||||
if status not in 'In Location':
|
||||
operand = 'in'
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,70 +1,184 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:criteria_name",
|
||||
"creation": "2017-05-29 01:32:43.064891",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"criteria_name",
|
||||
"max_score",
|
||||
"formula",
|
||||
"weight"
|
||||
],
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:criteria_name",
|
||||
"beta": 0,
|
||||
"creation": "2017-05-29 01:32:43.064891",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "criteria_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Criteria Name",
|
||||
"reqd": 1,
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "criteria_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Criteria Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"default": "100",
|
||||
"fieldname": "max_score",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Max Score",
|
||||
"reqd": 1
|
||||
},
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "100",
|
||||
"fieldname": "max_score",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Max Score",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "formula",
|
||||
"fieldtype": "Small Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Criteria Formula",
|
||||
"reqd": 1
|
||||
},
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "formula",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 1,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Criteria Formula",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "weight",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Criteria Weight"
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "weight",
|
||||
"fieldtype": "Percent",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Criteria Weight",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-11-11 18:34:58.477648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Scorecard Criteria",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-01-22 10:47:00.000822",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Scorecard Criteria",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
@@ -41,13 +41,10 @@ def get_conditions(filters):
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
|
||||
|
||||
for field in ['company', 'name']:
|
||||
for field in ['company', 'name', 'status']:
|
||||
if filters.get(field):
|
||||
conditions += f" and po.{field} = %({field})s"
|
||||
|
||||
if filters.get('status'):
|
||||
conditions += " and po.status in %(status)s"
|
||||
|
||||
if filters.get('project'):
|
||||
conditions += " and poi.project = %(project)s"
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Version 13.14.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- KSA E-Invoicing and VAT Report ([#27369](https://github.com/frappe/erpnext/pull/27369))
|
||||
- Added KSA VAT settings to setup KSA VAT accounts
|
||||
- New report KSA VAT to check the vat amounts
|
||||
- Print format for KSA VAT Invoice ([#28166](https://github.com/frappe/erpnext/pull/28166))
|
||||
|
||||
- Provision to setup tax for recurring additional salary in Salary Slip ([#27459](https://github.com/frappe/erpnext/pull/27459))
|
||||
- Add dispatch address in E-invoicing for India localization ([#28084](https://github.com/frappe/erpnext/pull/28084))
|
||||
- Employee initial work history updated when transfer is performed ([#27768](https://github.com/frappe/erpnext/pull/27768))
|
||||
- Provision to setup quality inspection teamplte in the operation which will be use in the Job Card([#28219](https://github.com/frappe/erpnext/pull/28219))
|
||||
- Improved sales invoice submission performance ([#27916](https://github.com/frappe/erpnext/pull/27916))
|
||||
|
||||
|
||||
### Fixes
|
||||
|
||||
- Splitting outstanding rows as per payment terms ([#27946](https://github.com/frappe/erpnext/pull/27946))
|
||||
|
||||
- Make status filter in Fixed Asset Register optional ([#28126](https://github.com/frappe/erpnext/pull/28126))
|
||||
- Skip empty rows while updating unsaved BOM cost ([#28136](https://github.com/frappe/erpnext/pull/28136))
|
||||
- TDS round off not working from second transaction ([#27934](https://github.com/frappe/erpnext/pull/27934))
|
||||
- Update receivable/payable account on company change in the Sales / Purchase Invoice ([#28057](https://github.com/frappe/erpnext/pull/28057))
|
||||
- Changes in Maintenance Schedule gets overwritten on save ([#27990](https://github.com/frappe/erpnext/pull/27990))
|
||||
- Fetch thumbnail from Item master instead of regenerating ([#28005](https://github.com/frappe/erpnext/pull/28005))
|
||||
- Serial Nos not set in the row after scanning in popup ([#28202](https://github.com/frappe/erpnext/pull/28202))
|
||||
- Taxjar customer_address fix, currency fix ([#28262](https://github.com/frappe/erpnext/pull/28262))
|
||||
- TaxJar update - added nexus list, making api call only for nexus ([#27497](https://github.com/frappe/erpnext/pull/27497))
|
||||
- Don't reset rates in Timesheet Detail when Activity Type is cleared ([#28056](https://github.com/frappe/erpnext/pull/28056))
|
||||
- Show full item name in search widget ([#28283](https://github.com/frappe/erpnext/pull/28283))
|
||||
- Avoid automatic customer creation on website user login ([#27914](https://github.com/frappe/erpnext/pull/27914))
|
||||
- POS Closing Entry without linked invoices ([#28042](https://github.com/frappe/erpnext/pull/28042))
|
||||
- Added patch to fix production plan status ([#27567](https://github.com/frappe/erpnext/pull/27567))
|
||||
- Interstate internal transfer invoices was not displying in the GSTR-1 report ([#27970](https://github.com/frappe/erpnext/pull/27970))
|
||||
- Shows opening balance from filtered from date in the stock balance and stock ledger report ([#26877](https://github.com/frappe/erpnext/pull/26877))
|
||||
- Employee filter in YTD and MTD in salary slip ([#27997](https://github.com/frappe/erpnext/pull/27997))
|
||||
- Removed warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195))
|
||||
- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967))
|
||||
- Autoemail report not showing dynamic report filters ([#28114](https://github.com/frappe/erpnext/pull/28114))
|
||||
- Incorrect VAT Amount in UAE VAT 201 report ([#27994](https://github.com/frappe/erpnext/pull/27994))
|
||||
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation([#27728](https://github.com/frappe/erpnext/pull/27728))
|
||||
@@ -1,36 +0,0 @@
|
||||
# Version 13.15.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Add count for Healthcare Practitioner on Healthcare dashboard
|
||||
([#28286](https://github.com/frappe/erpnext/pull/28286))
|
||||
- Improved financial statement report loading time ([#28238](https://github.com/frappe/erpnext/pull/28238))
|
||||
- Improved general ledger report loading time ([#27987](https://github.com/frappe/erpnext/pull/27987))
|
||||
- Replaced "=" with "in" for multiple statuses in query ([#28193](https://github.com/frappe/erpnext/pull/28193))
|
||||
- Update rate in the item price if the Update Existing Price List Rate is enabled in the stock settings ([#28255](https://github.com/frappe/erpnext/pull/28255))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Serial Nos not set in the row after scanning in popup ([#28202](https://github.com/frappe/erpnext/pull/28202))
|
||||
- Help section background in dark mode ([#28406](https://github.com/frappe/erpnext/pull/28406))
|
||||
- Don't make naming series mandatory for items ([#28394](https://github.com/frappe/erpnext/pull/28394))
|
||||
- Work order creation from sales order ([#28388](https://github.com/frappe/erpnext/pull/28388))
|
||||
- Workspace links to ecommerce settings ([#28360](https://github.com/frappe/erpnext/pull/28360))
|
||||
- Currency wise pricing rule was not working ([#28417](https://github.com/frappe/erpnext/pull/28417))
|
||||
- Bug with qrcode generation for the Urdu language ([#28471](https://github.com/frappe/erpnext/pull/28471))
|
||||
- Removed item - item group name validation ([#28392](https://github.com/frappe/erpnext/pull/28392))
|
||||
- Silter only submitted fees in student fee collection report ([#28280](https://github.com/frappe/erpnext/pull/28280))
|
||||
- Update tax template name for 18% GST ([#28156](https://github.com/frappe/erpnext/pull/28156))
|
||||
- Get credit amount for bank account of type liability ([#28132](https://github.com/frappe/erpnext/pull/28132))
|
||||
- Default party account getting overriden in invoices ([#28363](https://github.com/frappe/erpnext/pull/28363))
|
||||
- Remove warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195))
|
||||
- POS idx issue in taxes table while merging ([#28389](https://github.com/frappe/erpnext/pull/28389))
|
||||
- Address not set in the Dispatch Address field ([#28333](https://github.com/frappe/erpnext/pull/28333))
|
||||
- Not able to edit the supplier scorecard criteria name once created ([#28348](https://github.com/frappe/erpnext/pull/28348))
|
||||
- GST category not getting auto updated ([#28459](https://github.com/frappe/erpnext/pull/28459))
|
||||
- Sales Invoice with duplicate items not showing correct taxable value ([#28334](https://github.com/frappe/erpnext/pull/28334))
|
||||
- KSA Invoice print format for multicurrency invoices ([#28489](https://github.com/frappe/erpnext/pull/28489))
|
||||
- Performance issue while submitting the Journal Entry ([#28425](https://github.com/frappe/erpnext/pull/28425))
|
||||
- Pricing Rule not created against the Promotional Scheme ([#28398](https://github.com/frappe/erpnext/pull/28398))
|
||||
- Pull only Items that are in Job Card in a Stock Entry against Job Card ([#28228](https://github.com/frappe/erpnext/pull/28228))
|
||||
- Fixed sum of components in salary register ([#28237](https://github.com/frappe/erpnext/pull/28237))
|
||||
@@ -820,38 +820,6 @@ 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'):
|
||||
@@ -1069,15 +1037,15 @@ 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), indicator="orange", alert=True)
|
||||
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
|
||||
|
||||
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")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
def get_company_default(self, fieldname):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)
|
||||
return get_company_default(self.company, fieldname)
|
||||
|
||||
def get_stock_items(self):
|
||||
stock_items = []
|
||||
@@ -1391,8 +1359,8 @@ class AccountsController(TransactionBase):
|
||||
total = 0
|
||||
base_total = 0
|
||||
for d in self.get("payment_schedule"):
|
||||
total += flt(d.payment_amount, d.precision("payment_amount"))
|
||||
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
|
||||
total += flt(d.payment_amount)
|
||||
base_total += flt(d.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
|
||||
@@ -1408,9 +1376,8 @@ 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 flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total")) > 0.1 or \
|
||||
flt(base_total, self.precision("base_grand_total")) - flt(base_grand_total, self.precision("base_grand_total")) > 0.1:
|
||||
if total != flt(grand_total, self.precision("grand_total")) or \
|
||||
base_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,8 +132,7 @@ 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
|
||||
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
@@ -211,15 +210,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
# these are handled separately
|
||||
ignored_search_fields = ("item_name", "description")
|
||||
for ignored_field in ignored_search_fields:
|
||||
if ignored_field in searchfields:
|
||||
searchfields.remove(ignored_field)
|
||||
if "description" in searchfields:
|
||||
searchfields.remove("description")
|
||||
|
||||
columns = ''
|
||||
extra_searchfields = [field for field in searchfields
|
||||
if not field in ["name", "item_group", "description", "item_name"]]
|
||||
if not field in ["name", "item_group", "description"]]
|
||||
|
||||
if extra_searchfields:
|
||||
columns = ", " + ", ".join(extra_searchfields)
|
||||
@@ -256,8 +252,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
if frappe.db.count('Item', cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = 'or tabItem.description LIKE %(txt)s'
|
||||
return frappe.db.sql("""select
|
||||
tabItem.name, tabItem.item_name, tabItem.item_group,
|
||||
return frappe.db.sql("""select tabItem.name,
|
||||
if(length(tabItem.item_name) > 40,
|
||||
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
|
||||
tabItem.item_group,
|
||||
if(length(tabItem.description) > 40, \
|
||||
concat(substr(tabItem.description, 1, 40), "..."), description) as description
|
||||
{columns}
|
||||
@@ -567,7 +565,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
|
||||
|
||||
query_filters.append(['name', query_selector, dimensions])
|
||||
|
||||
output = frappe.get_list(doctype, filters=query_filters)
|
||||
output = frappe.get_all(doctype, filters=query_filters)
|
||||
result = [d.name for d in output]
|
||||
|
||||
return [(d,) for d in set(result)]
|
||||
|
||||
@@ -216,14 +216,11 @@ 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:
|
||||
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
|
||||
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
|
||||
item['reduce_by'] = item[args['target_field']] - item['max_allowed']
|
||||
|
||||
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)
|
||||
self.limits_crossed_error(args, item, qty_or_amount)
|
||||
|
||||
def limits_crossed_error(self, args, item, qty_or_amount):
|
||||
'''Raise exception for limits crossed'''
|
||||
@@ -241,19 +238,6 @@ 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,9 +260,7 @@ 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):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
self.doc.rounding_adjustment = 0
|
||||
|
||||
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"])
|
||||
@@ -314,9 +312,7 @@ 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 not self.doc.get('is_consolidated'):
|
||||
and self.doc.discount_amount and self.doc.apply_discount_on == "Grand Total":
|
||||
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
||||
- flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
@@ -409,16 +405,11 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.rounding_adjustment = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
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.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.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
|
||||
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"])
|
||||
|
||||
@@ -455,20 +446,19 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.total_net_weight += d.total_weight
|
||||
|
||||
def set_rounded_total(self):
|
||||
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
|
||||
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,8 +305,6 @@ 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,6 +1,7 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -95,32 +96,38 @@ 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
|
||||
]
|
||||
|
||||
if not attributes:
|
||||
return []
|
||||
valid_attributes = []
|
||||
|
||||
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,
|
||||
)
|
||||
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]
|
||||
])
|
||||
|
||||
attribute_value_map = {}
|
||||
for d in result:
|
||||
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
|
||||
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
|
||||
selected_attributes.append(attr)
|
||||
|
||||
out = []
|
||||
for name, values in attribute_value_map.items():
|
||||
out.append(frappe._dict(name=name, item_attribute_values=values))
|
||||
return out
|
||||
if selected_attributes:
|
||||
valid_attributes.append(
|
||||
_dict(
|
||||
item_attribute_values=selected_attributes,
|
||||
name=attr_doc.name
|
||||
)
|
||||
)
|
||||
|
||||
return valid_attributes
|
||||
|
||||
def get_discount_filters(self, discounts):
|
||||
discount_filters = []
|
||||
@@ -140,4 +147,4 @@ class ProductFiltersBuilder:
|
||||
label = f"{discount}% and below"
|
||||
discount_filters.append([discount, label])
|
||||
|
||||
return discount_filters
|
||||
return discount_filters
|
||||
@@ -175,7 +175,9 @@ class TestProductDataEngine(unittest.TestCase):
|
||||
|
||||
filter_engine = ProductFiltersBuilder()
|
||||
attribute_filter = filter_engine.get_attribute_filters()[0]
|
||||
attribute_values = attribute_filter.item_attribute_values
|
||||
attributes = attribute_filter.item_attribute_values
|
||||
|
||||
attribute_values = [d.attribute_value for d in attributes]
|
||||
|
||||
self.assertEqual(attribute_filter.name, "Test Size")
|
||||
self.assertGreater(len(attribute_values), 0)
|
||||
@@ -347,4 +349,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)
|
||||
@@ -201,9 +201,7 @@ def add_new_address(doc):
|
||||
def create_lead_for_item_inquiry(lead, subject, message):
|
||||
lead = frappe.parse_json(lead)
|
||||
lead_doc = frappe.new_doc('Lead')
|
||||
for fieldname in ("lead_name", "company_name", "email_id", "phone"):
|
||||
lead_doc.set(fieldname, lead.get(fieldname))
|
||||
|
||||
lead_doc.update(lead)
|
||||
lead_doc.set('lead_owner', '')
|
||||
|
||||
if not frappe.db.exists('Lead Source', 'Product Inquiry'):
|
||||
@@ -211,7 +209,6 @@ def create_lead_for_item_inquiry(lead, subject, message):
|
||||
'doctype': 'Lead Source',
|
||||
'source_name' : 'Product Inquiry'
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
lead_doc.set('source', 'Product Inquiry')
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
|
||||
@@ -14,19 +17,10 @@ def show_cart_count():
|
||||
return False
|
||||
|
||||
def set_cart_count(login_manager):
|
||||
# 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
|
||||
|
||||
role, parties = check_customer_or_supplier()
|
||||
if role == 'Supplier': 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):
|
||||
@@ -37,13 +31,13 @@ def update_website_context(context):
|
||||
cart_enabled = is_cart_enabled()
|
||||
context["shopping_cart_enabled"] = cart_enabled
|
||||
|
||||
def is_customer():
|
||||
if frappe.session.user and frappe.session.user != "Guest":
|
||||
def check_customer_or_supplier():
|
||||
if frappe.session.user:
|
||||
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 == 'Customer':
|
||||
return True
|
||||
if link.link_doctype in ('Customer', 'Supplier'):
|
||||
return link.link_doctype, link.link_name
|
||||
|
||||
return False
|
||||
return 'Customer', None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"creation": "2016-06-22 02:58:41.024538",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
@@ -13,7 +13,7 @@
|
||||
"name": "Student Fee Collection",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n student as \"Student:Link/Student:200\",\n student_name as \"Student Name::200\",\n sum(grand_total) - sum(outstanding_amount) as \"Paid Amount:Currency:150\",\n sum(outstanding_amount) as \"Outstanding Amount:Currency:150\",\n sum(grand_total) as \"Grand Total:Currency:150\"\nFROM\n `tabFees` \nWHERE\n docstatus=1 \nGROUP BY\n student",
|
||||
"query": "SELECT\n student as \"Student:Link/Student:200\",\n student_name as \"Student Name::200\",\n sum(grand_total) - sum(outstanding_amount) as \"Paid Amount:Currency:150\",\n sum(outstanding_amount) as \"Outstanding Amount:Currency:150\",\n sum(grand_total) as \"Grand Total:Currency:150\"\nFROM\n `tabFees` \nGROUP BY\n student",
|
||||
"ref_doctype": "Fees",
|
||||
"report_name": "Student Fee Collection",
|
||||
"report_type": "Query Report",
|
||||
@@ -22,4 +22,4 @@
|
||||
"role": "Academics User"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,8 @@ def verify_request():
|
||||
)
|
||||
|
||||
if frappe.request.data and \
|
||||
not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode():
|
||||
frappe.get_request_header("X-Wc-Webhook-Signature") and \
|
||||
not sig == bytes(frappe.get_request_header("X-Wc-Webhook-Signature").encode()):
|
||||
frappe.throw(_("Unverified Webhook Data"))
|
||||
frappe.set_user(woocommerce_settings.creation_user)
|
||||
|
||||
|
||||
@@ -143,9 +143,6 @@ def verify_transaction(**kwargs):
|
||||
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||
|
||||
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||||
if not isinstance(checkout_id, str):
|
||||
frappe.throw(_("Invalid Checkout Request ID"))
|
||||
|
||||
integration_request = frappe.get_doc("Integration Request", checkout_id)
|
||||
transaction_data = frappe._dict(loads(integration_request.data))
|
||||
total_paid = 0 # for multiple integration request made against a pos invoice
|
||||
@@ -236,9 +233,6 @@ def process_balance_info(**kwargs):
|
||||
account_balance_response = frappe._dict(kwargs["Result"])
|
||||
|
||||
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
||||
if not isinstance(conversation_id, str):
|
||||
frappe.throw(_("Invalid Conversation ID"))
|
||||
|
||||
request = frappe.get_doc("Integration Request", conversation_id)
|
||||
|
||||
if request.status == "Completed":
|
||||
|
||||
@@ -68,8 +68,5 @@ def dump_request_data(data, event="create/order"):
|
||||
@frappe.whitelist()
|
||||
def resync(method, name, request_data):
|
||||
frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False)
|
||||
if not method.startswith("erpnext.erpnext_integrations.connectors.shopify_connection"):
|
||||
return
|
||||
|
||||
frappe.enqueue(method=method, queue='short', timeout=300, is_async=True,
|
||||
**{"order": json.loads(request_data), "request_id": name})
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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,33 +5,5 @@ 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);
|
||||
},
|
||||
|
||||
on_load: (frm) => {
|
||||
frm.set_query('shipping_account_head', function() {
|
||||
return {
|
||||
filters: {
|
||||
'company': frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('tax_account_head', function() {
|
||||
return {
|
||||
filters: {
|
||||
'company': frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frm.add_custom_button(__('Update Nexus List'), function() {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'update_nexus_list'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,22 +6,17 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"taxjar_calculate_tax",
|
||||
"is_sandbox",
|
||||
"taxjar_calculate_tax",
|
||||
"taxjar_create_transactions",
|
||||
"credentials",
|
||||
"api_key",
|
||||
"cb_keys",
|
||||
"sandbox_api_key",
|
||||
"configuration",
|
||||
"company",
|
||||
"column_break_10",
|
||||
"tax_account_head",
|
||||
"configuration_cb",
|
||||
"shipping_account_head",
|
||||
"section_break_12",
|
||||
"nexus_address",
|
||||
"nexus"
|
||||
"shipping_account_head"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -59,7 +54,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "taxjar_calculate_tax",
|
||||
"fieldname": "is_sandbox",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sandbox Mode"
|
||||
@@ -69,9 +63,12 @@
|
||||
"fieldtype": "Password",
|
||||
"label": "Sandbox API Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "configuration_cb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "taxjar_calculate_tax",
|
||||
"fieldname": "taxjar_create_transactions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create TaxJar Transaction"
|
||||
@@ -85,42 +82,11 @@
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"fieldname": "configuration_cb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-08 18:02:29.232090",
|
||||
"modified": "2020-04-30 04:38:03.311089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "TaxJar Settings",
|
||||
|
||||
@@ -4,98 +4,9 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
# import frappe
|
||||
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):
|
||||
|
||||
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, options='currency'),
|
||||
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
|
||||
label='Taxable Amount', read_only=1, options='currency')
|
||||
],
|
||||
'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)
|
||||
pass
|
||||
|
||||
@@ -4,9 +4,9 @@ import frappe
|
||||
import taxjar
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext import get_default_company, get_region
|
||||
from erpnext import get_default_company
|
||||
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
|
||||
@@ -21,7 +21,6 @@ SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', '
|
||||
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
|
||||
|
||||
|
||||
|
||||
def get_client():
|
||||
taxjar_settings = frappe.get_single("TaxJar Settings")
|
||||
|
||||
@@ -104,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, doc.docstatus) for item in doc.items]
|
||||
line_items = [get_line_item_dict(item) for item in doc.items]
|
||||
|
||||
if from_shipping_state not in SUPPORTED_STATE_CODES:
|
||||
from_shipping_state = get_state_code(from_address, 'Company')
|
||||
@@ -140,28 +139,18 @@ def get_state_code(address, location):
|
||||
|
||||
return state_code
|
||||
|
||||
def get_line_item_dict(item, docstatus):
|
||||
tax_dict = dict(
|
||||
def get_line_item_dict(item):
|
||||
return 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
|
||||
|
||||
if get_region(doc.company) != 'United States':
|
||||
return
|
||||
|
||||
if not doc.items:
|
||||
return
|
||||
|
||||
@@ -175,9 +164,6 @@ 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:
|
||||
@@ -205,17 +191,6 @@ 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 \
|
||||
@@ -266,7 +241,7 @@ def get_shipping_address_details(doc):
|
||||
if doc.shipping_address_name:
|
||||
shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
|
||||
elif doc.customer_address:
|
||||
shipping_address = frappe.get_doc("Address", doc.customer_address)
|
||||
shipping_address = frappe.get_doc("Address", doc.customer_address_name)
|
||||
else:
|
||||
shipping_address = get_company_address_details(doc)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
||||
)
|
||||
|
||||
if frappe.request.data and \
|
||||
frappe.get_request_header(hmac_key) and \
|
||||
not sig == bytes(frappe.get_request_header(hmac_key).encode()):
|
||||
frappe.throw(_("Unverified Webhook Data"))
|
||||
frappe.set_user(settings.modified_by)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "healthcare",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Healthcare",
|
||||
"links": [
|
||||
@@ -535,7 +534,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-29 14:31:16.795628",
|
||||
"modified": "2021-01-30 19:35:45.316999",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Healthcare",
|
||||
@@ -570,12 +569,8 @@
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"color": "#29CD42",
|
||||
"doc_view": "List",
|
||||
"format": "{} Active",
|
||||
"label": "Healthcare Practitioner",
|
||||
"link_to": "Healthcare Practitioner",
|
||||
"stats_filter": "{\n \"status\": \"Active\"\n}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -260,13 +260,11 @@ doc_events = {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.regional.italy.utils.sales_invoice_on_submit",
|
||||
"erpnext.regional.saudi_arabia.utils.create_qr_code",
|
||||
"erpnext.erpnext_integrations.taxjar_integration.create_transaction"
|
||||
],
|
||||
"on_cancel": [
|
||||
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
|
||||
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction",
|
||||
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
|
||||
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
|
||||
],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||
"validate": [
|
||||
@@ -314,8 +312,7 @@ doc_events = {
|
||||
'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"]
|
||||
},
|
||||
"Company": {
|
||||
"on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company",
|
||||
"erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]
|
||||
"on_trash": "erpnext.regional.india.utils.delete_gst_settings_for_company"
|
||||
},
|
||||
"Integration Request": {
|
||||
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
|
||||
|
||||
@@ -156,8 +156,6 @@ 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'
|
||||
""",
|
||||
@@ -168,8 +166,6 @@ 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,7 +55,6 @@ 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_work_history, validate_active_employee
|
||||
from erpnext.hr.utils import update_employee, 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_work_history(employee, self.promotion_details, date=self.promotion_date)
|
||||
employee = update_employee(employee, self.promotion_details, date=self.promotion_date)
|
||||
employee.save()
|
||||
|
||||
def on_cancel(self):
|
||||
employee = frappe.get_doc("Employee", self.employee)
|
||||
employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
|
||||
employee = update_employee(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_work_history
|
||||
from erpnext.hr.utils import update_employee
|
||||
|
||||
|
||||
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_work_history(new_employee, self.transfer_details, date=self.transfer_date)
|
||||
new_employee = update_employee(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_work_history(employee, self.transfer_details, date=self.transfer_date)
|
||||
employee = update_employee(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_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
|
||||
employee = update_employee(employee, self.transfer_details, cancel=True)
|
||||
if self.new_company != self.company:
|
||||
employee.company = self.company
|
||||
employee.save()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
@@ -16,12 +15,7 @@ class TestEmployeeTransfer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee2@transfers.com")
|
||||
make_employee("employee3@transfers.com")
|
||||
create_company()
|
||||
create_employee()
|
||||
create_employee_transfer()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.sql("""delete from `tabEmployee Transfer`""")
|
||||
|
||||
def test_submit_before_transfer_date(self):
|
||||
transfer_obj = frappe.get_doc({
|
||||
@@ -63,77 +57,3 @@ 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": "2021-10-26 20:27:36.027728",
|
||||
"modified": "2020-09-23 20:27:36.027728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Expense Taxes and Charges",
|
||||
|
||||
@@ -145,15 +145,7 @@ 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_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
|
||||
})
|
||||
|
||||
def update_employee(employee, details, date=None, cancel=False):
|
||||
internal_work_history = {}
|
||||
for item in details:
|
||||
field = frappe.get_meta("Employee").get_field(item.fieldname)
|
||||
@@ -168,35 +160,11 @@ def update_employee_work_history(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": "Website Image"
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -539,7 +539,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-27 14:52:04.500251",
|
||||
"modified": "2021-05-16 12:25:09.081968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -308,9 +308,6 @@ 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,
|
||||
@@ -603,7 +600,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)
|
||||
elif d.item_code:
|
||||
else:
|
||||
self.add_to_cur_exploded_items(frappe._dict({
|
||||
'item_code' : d.item_code,
|
||||
'item_name' : d.item_name,
|
||||
|
||||
@@ -28,11 +28,6 @@ frappe.ui.form.on('Job Card', {
|
||||
frappe.flags.resume_job = 0;
|
||||
let has_items = frm.doc.items && frm.doc.items.length;
|
||||
|
||||
if (frm.doc.__onload.work_order_stopped) {
|
||||
frm.disable_save();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
|
||||
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
@@ -75,23 +70,6 @@ frappe.ui.form.on('Job Card', {
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
frm.trigger("setup_quality_inspection");
|
||||
},
|
||||
|
||||
setup_quality_inspection: function(frm) {
|
||||
let quality_inspection_field = frm.get_docfield("quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(frm) {
|
||||
return {
|
||||
"inspection_type": "In Process",
|
||||
"reference_type": "Job Card",
|
||||
"reference_name": frm.doc.name,
|
||||
"item_code": frm.doc.production_item,
|
||||
"item_name": frm.doc.item_name,
|
||||
"item_serial_no": frm.doc.serial_no,
|
||||
"batch_no": frm.doc.batch_no,
|
||||
"quality_inspection_template": frm.doc.quality_inspection_template,
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
setup_corrective_job_card: function(frm) {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"serial_no",
|
||||
"column_break_12",
|
||||
"wip_warehouse",
|
||||
"quality_inspection_template",
|
||||
"quality_inspection",
|
||||
"project",
|
||||
"batch_no",
|
||||
@@ -397,7 +396,6 @@
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "scrap_items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Scrap Items"
|
||||
@@ -409,18 +407,11 @@
|
||||
"no_copy": 1,
|
||||
"options": "Job Card Scrap Item",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "operation.quality_inspection_template",
|
||||
"fieldname": "quality_inspection_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection Template",
|
||||
"options": "Quality Inspection Template"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-12 10:15:06.572401",
|
||||
"modified": "2021-09-14 00:38:46.873105",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -37,7 +37,6 @@ class JobCard(Document):
|
||||
def onload(self):
|
||||
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||
self.set_onload("job_card_excess_transfer", excess_transfer)
|
||||
self.set_onload("work_order_stopped", self.is_work_order_stopped())
|
||||
|
||||
def validate(self):
|
||||
self.validate_time_logs()
|
||||
@@ -46,7 +45,6 @@ class JobCard(Document):
|
||||
self.validate_sequence_id()
|
||||
self.set_sub_operations()
|
||||
self.update_sub_operation_status()
|
||||
self.validate_work_order()
|
||||
|
||||
def set_sub_operations(self):
|
||||
if self.operation:
|
||||
@@ -505,11 +503,13 @@ class JobCard(Document):
|
||||
self.status = 'Work In Progress'
|
||||
|
||||
if (self.docstatus == 1 and
|
||||
(self.for_quantity <= self.total_completed_qty or not self.items)):
|
||||
(self.for_quantity <= self.transferred_qty or not self.items)):
|
||||
# consider excess transfer
|
||||
# completed qty is checked via separate validation
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status != 'Completed':
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
if self.for_quantity == self.transferred_qty:
|
||||
self.status = 'Material Transferred'
|
||||
|
||||
if update_status:
|
||||
@@ -549,18 +549,6 @@ class JobCard(Document):
|
||||
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
||||
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_stopped():
|
||||
frappe.throw(_("You can't make any changes to Job Card since Work Order is stopped."))
|
||||
|
||||
def is_work_order_stopped(self):
|
||||
if self.work_order:
|
||||
status = frappe.get_value('Work Order', self.work_order)
|
||||
|
||||
if status == "Closed":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_time_log(args):
|
||||
@@ -617,8 +605,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {
|
||||
"required_qty": "qty",
|
||||
"uom": "stock_uom",
|
||||
"name": "job_card_item"
|
||||
"uom": "stock_uom"
|
||||
},
|
||||
"postprocess": update_item,
|
||||
}
|
||||
@@ -628,22 +615,17 @@ def make_material_request(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name, target_doc=None):
|
||||
def update_item(source, target, source_parent):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.t_warehouse = source_parent.wip_warehouse
|
||||
|
||||
if not target.conversion_factor:
|
||||
target.conversion_factor = 1
|
||||
|
||||
pending_rm_qty = flt(source.required_qty) - flt(source.transferred_qty)
|
||||
if pending_rm_qty > 0:
|
||||
target.qty = pending_rm_qty
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.purpose = "Material Transfer for Manufacture"
|
||||
target.from_bom = 1
|
||||
|
||||
# avoid negative 'For Quantity'
|
||||
pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0))
|
||||
pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
|
||||
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
|
||||
|
||||
target.set_transfer_qty()
|
||||
|
||||
@@ -16,22 +16,11 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestJobCard(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
make_bom_for_jc_tests()
|
||||
|
||||
transfer_material_against, source_warehouse = None, None
|
||||
|
||||
tests_that_skip_setup = (
|
||||
"test_job_card_material_transfer_correctness",
|
||||
)
|
||||
tests_that_transfer_against_jc = (
|
||||
"test_job_card_multiple_materials_transfer",
|
||||
"test_job_card_excess_material_transfer",
|
||||
"test_job_card_partial_material_transfer"
|
||||
)
|
||||
|
||||
if self._testMethodName in tests_that_skip_setup:
|
||||
return
|
||||
tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
|
||||
"test_job_card_excess_material_transfer")
|
||||
|
||||
if self._testMethodName in tests_that_transfer_against_jc:
|
||||
transfer_material_against = "Job Card"
|
||||
@@ -202,132 +191,4 @@ class TestJobCard(unittest.TestCase):
|
||||
job_card.submit()
|
||||
|
||||
# JC is Completed with excess transfer
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
|
||||
def test_job_card_partial_material_transfer(self):
|
||||
"Test partial material transfer against Job Card"
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
|
||||
qty=25, basic_rate=100)
|
||||
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
|
||||
target="Stores - _TC", qty=15, basic_rate=100)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
|
||||
# partially transfer
|
||||
transfer_entry = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry.fg_completed_qty = 1
|
||||
transfer_entry.get_items()
|
||||
transfer_entry.insert()
|
||||
transfer_entry.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertEqual(job_card.transferred_qty, 1)
|
||||
self.assertEqual(transfer_entry.items[0].qty, 5)
|
||||
self.assertEqual(transfer_entry.items[1].qty, 3)
|
||||
|
||||
# transfer remaining
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
|
||||
self.assertEqual(transfer_entry_2.items[0].qty, 5)
|
||||
self.assertEqual(transfer_entry_2.items[1].qty, 3)
|
||||
|
||||
transfer_entry_2.insert()
|
||||
transfer_entry_2.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertEqual(job_card.transferred_qty, 2)
|
||||
|
||||
def test_job_card_material_transfer_correctness(self):
|
||||
"""
|
||||
1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
|
||||
2. Test impact of changing 'For Qty' in such a Stock Entry
|
||||
"""
|
||||
create_bom_with_multiple_operations()
|
||||
work_order = make_wo_with_transfer_against_jc()
|
||||
|
||||
job_card_name = frappe.db.get_value(
|
||||
"Job Card",
|
||||
{"work_order": work_order.name,"operation": "Test Operation A"}
|
||||
)
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
|
||||
self.assertEqual(len(job_card.items), 1)
|
||||
self.assertEqual(job_card.items[0].item_code, "_Test Item")
|
||||
|
||||
# check if right items are mapped in transfer entry
|
||||
transfer_entry = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry.insert()
|
||||
|
||||
self.assertEqual(len(transfer_entry.items), 1)
|
||||
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
||||
self.assertEqual(transfer_entry.items[0].qty, 4)
|
||||
|
||||
# change 'For Qty' and check impact on items table
|
||||
# no.of items should be the same with qty change
|
||||
transfer_entry.fg_completed_qty = 2
|
||||
transfer_entry.get_items()
|
||||
|
||||
self.assertEqual(len(transfer_entry.items), 1)
|
||||
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
||||
self.assertEqual(transfer_entry.items[0].qty, 2)
|
||||
|
||||
# rollback via tearDown method
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
|
||||
test_record = frappe.get_test_records("BOM")[2]
|
||||
bom_doc = frappe.get_doc(test_record)
|
||||
|
||||
row = {
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"hour_rate_rent": 300,
|
||||
"time_in_mins": 60
|
||||
}
|
||||
make_workstation(row)
|
||||
make_operation(row)
|
||||
|
||||
bom_doc.append("operations", {
|
||||
"operation": "Test Operation A",
|
||||
"description": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"hour_rate": 300,
|
||||
"time_in_mins": 60,
|
||||
"operating_cost": 100
|
||||
})
|
||||
|
||||
bom_doc.transfer_material_against = "Job Card"
|
||||
bom_doc.save()
|
||||
bom_doc.submit()
|
||||
|
||||
return bom_doc
|
||||
|
||||
def make_wo_with_transfer_against_jc():
|
||||
"Create a WO with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=4,
|
||||
transfer_material_against="Job Card",
|
||||
source_warehouse="Stores - _TC",
|
||||
do_not_submit=True
|
||||
)
|
||||
work_order.required_items[0].operation = "Test Operation A"
|
||||
work_order.required_items[1].operation = "_Test Operation 1"
|
||||
work_order.submit()
|
||||
|
||||
return work_order
|
||||
|
||||
def make_bom_for_jc_tests():
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
bom = frappe.copy_doc(test_records[2])
|
||||
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
|
||||
bom.rm_cost_as_per = "Valuation Rate"
|
||||
bom.items[0].uom = "_Test UOM 1"
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert()
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
@@ -13,7 +13,6 @@
|
||||
"is_corrective_operation",
|
||||
"job_card_section",
|
||||
"create_job_card_based_on_batch_size",
|
||||
"quality_inspection_template",
|
||||
"column_break_6",
|
||||
"batch_size",
|
||||
"sub_operations_section",
|
||||
@@ -93,18 +92,12 @@
|
||||
"fieldname": "is_corrective_operation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Corrective Operation"
|
||||
},
|
||||
{
|
||||
"fieldname": "quality_inspection_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection Template",
|
||||
"options": "Quality Inspection Template"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-03 13:49:39.114976",
|
||||
"modified": "2021-01-12 15:09:23.593338",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Operation",
|
||||
|
||||
@@ -18,13 +18,15 @@ def make_operation(*args, **kwargs):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists("Operation", args.operation):
|
||||
try:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Operation",
|
||||
"name": args.operation,
|
||||
"workstation": args.workstation
|
||||
})
|
||||
doc.insert()
|
||||
return doc
|
||||
|
||||
return frappe.get_doc("Operation", args.operation)
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Operation", args.operation)
|
||||
|
||||
@@ -105,7 +105,7 @@ frappe.ui.form.on('Production Plan', {
|
||||
}
|
||||
frm.trigger("material_requirement");
|
||||
|
||||
const projected_qty_formula = ` <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
|
||||
<tr><td style="padding-left:25px">
|
||||
<div>
|
||||
<h3 style="text-decoration: underline;">
|
||||
|
||||
@@ -311,7 +311,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if self.total_produced_qty > 0:
|
||||
self.status = "In Process"
|
||||
if self.check_have_work_orders_completed():
|
||||
if self.total_produced_qty >= self.total_planned_qty:
|
||||
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 = 'Yes'
|
||||
po.is_subcontracted_item = 'Yes'
|
||||
for row in po_list:
|
||||
args = {
|
||||
'item_code': row.production_item,
|
||||
@@ -575,15 +575,6 @@ 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):
|
||||
|
||||
@@ -12,7 +12,6 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
ItemHasVariantError,
|
||||
OverProductionError,
|
||||
StockOverProductionError,
|
||||
close_work_order,
|
||||
make_stock_entry,
|
||||
stop_unstop,
|
||||
)
|
||||
@@ -801,46 +800,6 @@ class TestWorkOrder(unittest.TestCase):
|
||||
if row.is_scrap_item:
|
||||
self.assertEqual(row.qty, 1)
|
||||
|
||||
def test_close_work_order(self):
|
||||
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
||||
'Test RM Item 2 for Closed WO']
|
||||
|
||||
company = '_Test Company with perpetual inventory'
|
||||
for item_code in items:
|
||||
create_item(item_code = item_code, is_stock_item = 1,
|
||||
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
|
||||
|
||||
item = 'Test FG Item for Closed WO'
|
||||
raw_materials = ['Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO']
|
||||
if not frappe.db.get_value('BOM', {'item': item}):
|
||||
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
|
||||
bom.with_operations = 1
|
||||
bom.append('operations', {
|
||||
'operation': '_Test Operation 1',
|
||||
'workstation': '_Test Workstation 1',
|
||||
'hour_rate': 20,
|
||||
'time_in_mins': 60
|
||||
})
|
||||
|
||||
bom.submit()
|
||||
|
||||
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
|
||||
job_cards = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
|
||||
|
||||
if len(job_cards) == len(bom.operations):
|
||||
for jc in job_cards:
|
||||
job_card_doc = frappe.get_doc('Job Card', jc)
|
||||
job_card_doc.append('time_logs', {
|
||||
'from_time': now(),
|
||||
'time_in_mins': 60,
|
||||
'completed_qty': job_card_doc.for_quantity
|
||||
})
|
||||
|
||||
job_card_doc.submit()
|
||||
|
||||
close_work_order(wo_order, "Closed")
|
||||
self.assertEqual(wo_order.get('status'), "Closed")
|
||||
|
||||
def update_job_card(job_card):
|
||||
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||
job_card_doc.set('scrap_items', [
|
||||
|
||||
@@ -135,26 +135,24 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.set_intro(__("Submit this Work Order for further processing."));
|
||||
}
|
||||
|
||||
if (frm.doc.status != "Closed") {
|
||||
if (frm.doc.docstatus===1) {
|
||||
frm.trigger('show_progress_for_items');
|
||||
frm.trigger('show_progress_for_operations');
|
||||
}
|
||||
if (frm.doc.docstatus===1) {
|
||||
frm.trigger('show_progress_for_items');
|
||||
frm.trigger('show_progress_for_operations');
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1
|
||||
&& frm.doc.operations && frm.doc.operations.length) {
|
||||
if (frm.doc.docstatus === 1
|
||||
&& frm.doc.operations && frm.doc.operations.length) {
|
||||
|
||||
const not_completed = frm.doc.operations.filter(d => {
|
||||
if (d.status != 'Completed') {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (not_completed && not_completed.length) {
|
||||
frm.add_custom_button(__('Create Job Card'), () => {
|
||||
frm.trigger("make_job_card");
|
||||
}).addClass('btn-primary');
|
||||
const not_completed = frm.doc.operations.filter(d => {
|
||||
if(d.status != 'Completed') {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if(not_completed && not_completed.length) {
|
||||
frm.add_custom_button(__('Create Job Card'), () => {
|
||||
frm.trigger("make_job_card");
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,22 +517,14 @@ frappe.ui.form.on("Work Order Operation", {
|
||||
erpnext.work_order = {
|
||||
set_custom_buttons: function(frm) {
|
||||
var doc = frm.doc;
|
||||
if (doc.docstatus === 1 && doc.status != "Closed") {
|
||||
frm.add_custom_button(__('Close'), function() {
|
||||
frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."),
|
||||
() => {
|
||||
erpnext.work_order.change_work_order_status(frm, "Closed");
|
||||
}
|
||||
);
|
||||
}, __("Status"));
|
||||
|
||||
if (doc.docstatus === 1) {
|
||||
if (doc.status != 'Stopped' && doc.status != 'Completed') {
|
||||
frm.add_custom_button(__('Stop'), function() {
|
||||
erpnext.work_order.change_work_order_status(frm, "Stopped");
|
||||
erpnext.work_order.stop_work_order(frm, "Stopped");
|
||||
}, __("Status"));
|
||||
} else if (doc.status == 'Stopped') {
|
||||
frm.add_custom_button(__('Re-open'), function() {
|
||||
erpnext.work_order.change_work_order_status(frm, "Resumed");
|
||||
erpnext.work_order.stop_work_order(frm, "Resumed");
|
||||
}, __("Status"));
|
||||
}
|
||||
|
||||
@@ -723,10 +713,9 @@ erpnext.work_order = {
|
||||
});
|
||||
},
|
||||
|
||||
change_work_order_status: function(frm, status) {
|
||||
let method_name = status=="Closed" ? "close_work_order" : "stop_unstop";
|
||||
stop_work_order: function(frm, status) {
|
||||
frappe.call({
|
||||
method: `erpnext.manufacturing.doctype.work_order.work_order.${method_name}`,
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
|
||||
freeze: true,
|
||||
freeze_message: __("Updating Work Order status"),
|
||||
args: {
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nClosed\nCancelled",
|
||||
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
@@ -182,7 +182,6 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1.0",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty To Manufacture",
|
||||
@@ -573,12 +572,10 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"migration_hash": "a18118963f4fcdb7f9d326de5f4063ba",
|
||||
"modified": "2021-10-29 15:12:32.203605",
|
||||
"modified": "2021-08-24 15:14:03.844937",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"nsm_parent_field": "parent_work_order",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
||||
@@ -175,7 +175,7 @@ class WorkOrder(Document):
|
||||
|
||||
def update_status(self, status=None):
|
||||
'''Update status of work order if unknown'''
|
||||
if status != "Stopped" and status != "Closed":
|
||||
if status != "Stopped":
|
||||
status = self.get_status(status)
|
||||
|
||||
if status != self.status:
|
||||
@@ -624,6 +624,7 @@ class WorkOrder(Document):
|
||||
def validate_operation_time(self):
|
||||
for d in self.operations:
|
||||
if not d.time_in_mins > 0:
|
||||
print(self.bom_no, self.production_item)
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
||||
|
||||
def update_required_items(self):
|
||||
@@ -684,7 +685,9 @@ class WorkOrder(Document):
|
||||
if not d.operation:
|
||||
d.operation = operation
|
||||
else:
|
||||
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or float('inf')):
|
||||
# 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):
|
||||
self.append('required_items', {
|
||||
'rate': item.rate,
|
||||
'amount': item.rate * item.qty,
|
||||
@@ -966,10 +969,6 @@ def stop_unstop(work_order, status):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
pro_order = frappe.get_doc("Work Order", work_order)
|
||||
|
||||
if pro_order.status == "Closed":
|
||||
frappe.throw(_("Closed Work Order can not be stopped or Re-opened"))
|
||||
|
||||
pro_order.update_status(status)
|
||||
pro_order.update_planned_qty()
|
||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||
@@ -1004,29 +1003,6 @@ def make_job_card(work_order, operations):
|
||||
if row.job_card_qty > 0:
|
||||
create_job_card(work_order, row, auto_create=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def close_work_order(work_order, status):
|
||||
if not frappe.has_permission("Work Order", "write"):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
work_order = frappe.get_doc("Work Order", work_order)
|
||||
if work_order.get("operations"):
|
||||
job_cards = frappe.get_list("Job Card",
|
||||
filters={
|
||||
"work_order": work_order.name,
|
||||
"status": "Work In Progress"
|
||||
}, pluck='name')
|
||||
|
||||
if job_cards:
|
||||
job_cards = ", ".join(job_cards)
|
||||
frappe.throw(_("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format(job_cards))
|
||||
|
||||
work_order.update_status(status)
|
||||
work_order.update_planned_qty()
|
||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||
work_order.notify_update()
|
||||
return work_order.status
|
||||
|
||||
def split_qty_based_on_batch_size(wo_doc, row, qty):
|
||||
if not cint(frappe.db.get_value("Operation",
|
||||
row.operation, "create_job_card_based_on_batch_size")):
|
||||
|
||||
@@ -90,7 +90,7 @@ def make_workstation(*args, **kwargs):
|
||||
args = frappe._dict(args)
|
||||
|
||||
workstation_name = args.workstation_name or args.workstation
|
||||
if not frappe.db.exists("Workstation", workstation_name):
|
||||
try:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": workstation_name
|
||||
@@ -100,5 +100,5 @@ def make_workstation(*args, **kwargs):
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
|
||||
@@ -24,7 +24,7 @@ def get_data(filters):
|
||||
}
|
||||
|
||||
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
|
||||
"total_completed_qty", "workstation", "operation", "total_time_in_mins"]
|
||||
"total_completed_qty", "workstation", "operation", "employee_name", "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"):
|
||||
filters=job_card_time_filter, group_by="parent", debug=1):
|
||||
job_card_time_details[job_card_data.parent] = job_card_data
|
||||
|
||||
res = []
|
||||
@@ -172,6 +172,12 @@ 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",
|
||||
|
||||
@@ -29,15 +29,8 @@ def get_production_plan_item_details(filters, data, order_details):
|
||||
|
||||
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||
for row in production_plan_doc.po_items:
|
||||
work_order = frappe.get_value(
|
||||
"Work Order",
|
||||
{
|
||||
"production_plan_item": row.name,
|
||||
"bom_no": row.bom_no,
|
||||
"production_item": row.item_code
|
||||
},
|
||||
"name"
|
||||
)
|
||||
work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
|
||||
"bom_no": row.bom_no, "production_item": row.item_code}, "name")
|
||||
|
||||
if row.item_code not in itemwise_indent:
|
||||
itemwise_indent.setdefault(row.item_code, {})
|
||||
@@ -48,10 +41,10 @@ def get_production_plan_item_details(filters, data, order_details):
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"qty": row.planned_qty,
|
||||
"document_type": "Work Order",
|
||||
"document_name": work_order or "",
|
||||
"document_name": work_order,
|
||||
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
|
||||
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
|
||||
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
|
||||
"produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
|
||||
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
|
||||
})
|
||||
|
||||
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
|
||||
@@ -62,23 +55,11 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_
|
||||
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
|
||||
|
||||
if subcontracted_item:
|
||||
docname = frappe.get_value(
|
||||
"Purchase Order Item",
|
||||
{
|
||||
"production_plan_sub_assembly_item": item.name,
|
||||
"docstatus": ("<", 2)
|
||||
},
|
||||
"parent"
|
||||
)
|
||||
docname = frappe.get_cached_value("Purchase Order Item",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
|
||||
else:
|
||||
docname = frappe.get_value(
|
||||
"Work Order",
|
||||
{
|
||||
"production_plan_sub_assembly_item": item.name,
|
||||
"docstatus": ("<", 2)
|
||||
},
|
||||
"name"
|
||||
)
|
||||
docname = frappe.get_cached_value("Work Order",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
|
||||
|
||||
data.append({
|
||||
"indent": 1,
|
||||
@@ -86,10 +67,10 @@ def get_production_plan_sub_assembly_item_details(filters, row, production_plan_
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
|
||||
"document_name": docname or "",
|
||||
"document_name": docname,
|
||||
"bom_level": item.bom_level,
|
||||
"produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0),
|
||||
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0))
|
||||
"produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
|
||||
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
|
||||
})
|
||||
|
||||
def get_work_order_details(filters, order_details):
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -51,7 +51,7 @@ frappe.query_reports["Work Order Summary"] = {
|
||||
label: __("Status"),
|
||||
fieldname: "status",
|
||||
fieldtype: "Select",
|
||||
options: ["", "Not Started", "In Process", "Completed", "Stopped", "Closed"]
|
||||
options: ["", "Not Started", "In Process", "Completed", "Stopped"]
|
||||
},
|
||||
{
|
||||
label: __("Sales Orders"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -59,16 +59,21 @@ def get_chart_data(data, filters):
|
||||
return get_chart_based_on_qty(data, filters)
|
||||
|
||||
def get_chart_based_on_status(data):
|
||||
labels = frappe.get_meta("Work Order").get_options("status").split("\n")
|
||||
if "" in labels:
|
||||
labels.remove("")
|
||||
labels = ["Completed", "In Process", "Stopped", "Not Started"]
|
||||
|
||||
status_wise_data = defaultdict(int)
|
||||
status_wise_data = {
|
||||
"Not Started": 0,
|
||||
"In Process": 0,
|
||||
"Stopped": 0,
|
||||
"Completed": 0,
|
||||
"Draft": 0
|
||||
}
|
||||
|
||||
for d in data:
|
||||
status_wise_data[d.status] += 1
|
||||
|
||||
values = [status_wise_data[label] for label in labels]
|
||||
values = [status_wise_data["Completed"], status_wise_data["In Process"],
|
||||
status_wise_data["Stopped"], status_wise_data["Not Started"]]
|
||||
|
||||
chart = {
|
||||
"data": {
|
||||
|
||||
@@ -306,9 +306,7 @@ 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 #08-11-2021
|
||||
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
|
||||
erpnext.patches.v13_0.create_website_items #30-09-2021
|
||||
@@ -329,8 +327,3 @@ 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.v13_0.update_job_card_status
|
||||
erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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
|
||||
)
|
||||
""")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user